@jhb.software/payload-alt-text-plugin 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -36
- package/dist/components/BulkGenerateAltTextsButton.js +3 -16
- package/dist/components/BulkGenerateAltTextsButton.js.map +1 -1
- package/dist/components/summarizeBulkGenerate.d.ts +26 -0
- package/dist/components/summarizeBulkGenerate.js +56 -0
- package/dist/components/summarizeBulkGenerate.js.map +1 -0
- package/dist/endpoints/bulkGenerateAltTexts.d.ts +18 -1
- package/dist/endpoints/bulkGenerateAltTexts.js +39 -16
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +17 -9
- package/dist/endpoints/generateAltText.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +20 -4
- package/dist/plugin.js.map +1 -1
- package/dist/resolvers/anthropic.d.ts +64 -0
- package/dist/resolvers/anthropic.js +140 -0
- package/dist/resolvers/anthropic.js.map +1 -0
- package/dist/resolvers/createVisionResolver.d.ts +148 -0
- package/dist/resolvers/createVisionResolver.js +300 -0
- package/dist/resolvers/createVisionResolver.js.map +1 -0
- package/dist/resolvers/mistral.d.ts +15 -1
- package/dist/resolvers/mistral.js +85 -250
- package/dist/resolvers/mistral.js.map +1 -1
- package/dist/resolvers/openAI.d.ts +22 -3
- package/dist/resolvers/openAI.js +57 -138
- package/dist/resolvers/openAI.js.map +1 -1
- package/dist/translations/de.js +12 -4
- package/dist/translations/de.js.map +1 -1
- package/dist/translations/en.js +12 -4
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/translation-schema.json +24 -8
- package/dist/types/AltTextPluginConfig.d.ts +71 -12
- package/dist/types/AltTextPluginConfig.js.map +1 -1
- package/dist/utilities/altTextHealth.d.ts +3 -1
- package/dist/utilities/altTextHealth.js +103 -12
- package/dist/utilities/altTextHealth.js.map +1 -1
- package/dist/utilities/resolveLocales.d.ts +15 -0
- package/dist/utilities/resolveLocales.js +38 -0
- package/dist/utilities/resolveLocales.js.map +1 -0
- package/dist/utilities/stableStringify.d.ts +9 -0
- package/dist/utilities/stableStringify.js +19 -0
- package/dist/utilities/stableStringify.js.map +1 -0
- package/package.json +4 -5
package/dist/resolvers/openAI.js
CHANGED
|
@@ -1,34 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { makeParseableResponseFormat } from 'openai/lib/parser.mjs';
|
|
3
|
-
import { z } from 'zod';
|
|
1
|
+
import { createVisionResolver, VisionProviderError } from './createVisionResolver.js';
|
|
4
2
|
/** @see https://platform.openai.com/docs/guides/images-vision */ const OPENAI_SUPPORTED_MIME_TYPES = [
|
|
5
3
|
'image/jpeg',
|
|
6
4
|
'image/png',
|
|
7
5
|
'image/gif',
|
|
8
6
|
'image/webp'
|
|
9
7
|
];
|
|
10
|
-
/**
|
|
11
|
-
* Creates a chat completion `JSONSchema` response format object from
|
|
12
|
-
* the given Zod schema.
|
|
13
|
-
*
|
|
14
|
-
* This is a temporary drop in replacement for the zodResponseFormat from openai/helpers/zod.ts
|
|
15
|
-
* because of issue https://github.com/openai/openai-node/issues/1576
|
|
16
|
-
*/ function zodResponseFormat(zodObject, name, props) {
|
|
17
|
-
return makeParseableResponseFormat({
|
|
18
|
-
type: 'json_schema',
|
|
19
|
-
json_schema: {
|
|
20
|
-
...props,
|
|
21
|
-
name,
|
|
22
|
-
schema: z.toJSONSchema(zodObject, {
|
|
23
|
-
target: 'draft-7'
|
|
24
|
-
}),
|
|
25
|
-
strict: true
|
|
26
|
-
}
|
|
27
|
-
}, (content)=>zodObject.parse(JSON.parse(content)));
|
|
28
|
-
}
|
|
29
8
|
/**
|
|
30
9
|
* Creates an OpenAI-based resolver for alt text generation.
|
|
31
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. When it is not, reach for a
|
|
13
|
+
* resolver that inlines the bytes instead (`mistralResolver`,
|
|
14
|
+
* `anthropicResolver`).
|
|
15
|
+
*
|
|
32
16
|
* @example
|
|
33
17
|
* ```typescript
|
|
34
18
|
* import { openAIResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
@@ -46,39 +30,15 @@ import { z } from 'zod';
|
|
|
46
30
|
* model: 'Qwen/Qwen2.5-VL-72B-Instruct',
|
|
47
31
|
* })
|
|
48
32
|
* ```
|
|
49
|
-
*/ export const openAIResolver = (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const getClient = ()=>openai ??= new OpenAI({
|
|
56
|
-
apiKey,
|
|
57
|
-
baseURL: baseUrl
|
|
58
|
-
});
|
|
59
|
-
return {
|
|
60
|
-
key: 'openai',
|
|
61
|
-
resolve: async ({ filename, imageThumbnailUrl, locale })=>{
|
|
62
|
-
try {
|
|
63
|
-
const modelResponseSchema = z.object({
|
|
64
|
-
altText: z.string().describe('A concise, descriptive alt text for the image'),
|
|
65
|
-
keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
|
|
66
|
-
});
|
|
67
|
-
const response = await getClient().chat.completions.parse({
|
|
68
|
-
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,
|
|
69
39
|
messages: [
|
|
70
40
|
{
|
|
71
|
-
content:
|
|
72
|
-
You are an expert at analyzing images and creating descriptive image alt text.
|
|
73
|
-
|
|
74
|
-
Please analyze the given image and provide the following:
|
|
75
|
-
- 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.
|
|
76
|
-
- A list of keywords that describe the content (e.g., ["Camel", "Palm trees", "Desert"]) as "keywords"
|
|
77
|
-
|
|
78
|
-
If a context is provided, use it to enhance the alt text.
|
|
79
|
-
|
|
80
|
-
Format your response as a JSON object. You must respond in the ${locale} language.
|
|
81
|
-
`,
|
|
41
|
+
content: resolvedInstructions,
|
|
82
42
|
role: 'system'
|
|
83
43
|
},
|
|
84
44
|
{
|
|
@@ -100,95 +60,54 @@ import { z } from 'zod';
|
|
|
100
60
|
}
|
|
101
61
|
],
|
|
102
62
|
model,
|
|
103
|
-
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
|
|
104
86
|
});
|
|
105
|
-
const result = response.choices[0]?.message?.parsed;
|
|
106
|
-
if (!result) {
|
|
107
|
-
return {
|
|
108
|
-
error: 'No result from OpenAI',
|
|
109
|
-
success: false
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
result,
|
|
114
|
-
success: true
|
|
115
|
-
};
|
|
116
|
-
} catch (error) {
|
|
117
|
-
console.error('Error generating alt text:', error);
|
|
118
|
-
return {
|
|
119
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
120
|
-
success: false
|
|
121
|
-
};
|
|
122
87
|
}
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}
|
|
125
100
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
altText: z.string().describe('A concise, descriptive alt text for the image'),
|
|
130
|
-
keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
|
|
131
|
-
})
|
|
132
|
-
])));
|
|
133
|
-
const response = await getClient().chat.completions.parse({
|
|
134
|
-
max_completion_tokens: 300,
|
|
135
|
-
messages: [
|
|
136
|
-
{
|
|
137
|
-
content: `
|
|
138
|
-
You are an expert at analyzing images and creating descriptive image alt text.
|
|
139
|
-
|
|
140
|
-
Please analyze the given image and provide the following in ${locales.join(', ')}:
|
|
141
|
-
- 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.
|
|
142
|
-
- A localized list of keywords that describe the content (e.g., ["Camel", "Palm trees", "Desert"]) as "keywords"
|
|
143
|
-
|
|
144
|
-
If a context is provided, use it to enhance the alt text.
|
|
145
|
-
|
|
146
|
-
Format your response as a JSON object with ${locales.join(', ')} keys, each containing "altText" and "keywords".
|
|
147
|
-
`,
|
|
148
|
-
role: 'system'
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
content: [
|
|
152
|
-
{
|
|
153
|
-
type: 'image_url',
|
|
154
|
-
image_url: {
|
|
155
|
-
url: imageThumbnailUrl
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
...filename ? [
|
|
159
|
-
{
|
|
160
|
-
type: 'text',
|
|
161
|
-
text: filename
|
|
162
|
-
}
|
|
163
|
-
] : []
|
|
164
|
-
],
|
|
165
|
-
role: 'user'
|
|
166
|
-
}
|
|
167
|
-
],
|
|
168
|
-
model,
|
|
169
|
-
response_format: zodResponseFormat(modelResponseSchema, 'data')
|
|
170
|
-
});
|
|
171
|
-
const result = response.choices[0]?.message?.parsed;
|
|
172
|
-
if (!result) {
|
|
173
|
-
return {
|
|
174
|
-
error: 'No result from OpenAI',
|
|
175
|
-
success: false
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
results: result,
|
|
180
|
-
success: true
|
|
181
|
-
};
|
|
182
|
-
} catch (error) {
|
|
183
|
-
console.error('Error generating bulk alt text:', error);
|
|
184
|
-
return {
|
|
185
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
186
|
-
success: false
|
|
187
|
-
};
|
|
101
|
+
return JSON.parse(content);
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error('OpenAI returned a response that was not valid JSON');
|
|
188
104
|
}
|
|
189
105
|
},
|
|
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 * 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\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 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 supportedMimeTypes: config.supportedMimeTypes ?? OPENAI_SUPPORTED_MIME_TYPES,\n }\n}\n"],"names":["OpenAI","makeParseableResponseFormat","z","OPENAI_SUPPORTED_MIME_TYPES","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;AAoCvB,+DAA+D,GAC/D,MAAMC,8BAA8B;IAAC;IAAc;IAAa;IAAa;CAAa;AAE1F;;;;;;CAMC,GACD,SAASC,kBACPC,SAAmB,EACnBC,IAAY,EACZC,KAA+E;IAE/E,OAAON,4BACL;QACEO,MAAM;QACNC,aAAa;YACX,GAAGF,KAAK;YACRD;YACAI,QAAQR,EAAES,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,IAAItB,OAAO;YAAEmB;YAAQK,SAASJ;QAAQ;IAElF,OAAO;QACLK,KAAK;QACLC,SAAS,OAAO,EACdC,QAAQ,EACRC,iBAAiB,EACjBC,MAAM,EACc;YACpB,IAAI;gBACF,MAAMC,sBAAsB5B,EAAE6B,MAAM,CAAC;oBACnCC,SAAS9B,EAAE+B,MAAM,GAAGC,QAAQ,CAAC;oBAC7BC,UAAUjC,EAAEkC,KAAK,CAAClC,EAAE+B,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,sBAAsB5B,EAAE6B,MAAM,CAClC0B,OAAOC,WAAW,CAChBF,QAAQG,GAAG,CAAC,CAAC9B,SAAW;wBACtBA;wBACA3B,EAAE6B,MAAM,CAAC;4BACPC,SAAS9B,EAAE+B,MAAM,GAAGC,QAAQ,CAAC;4BAC7BC,UAAUjC,EACPkC,KAAK,CAAClC,EAAE+B,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;QACAU,oBAAoB5C,OAAO4C,kBAAkB,IAAI3D;IACnD;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. When it is not, reach for a\n * resolver that inlines the bytes instead (`mistralResolver`,\n * `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"}
|
package/dist/translations/de.js
CHANGED
|
@@ -14,9 +14,15 @@ export const de = {
|
|
|
14
14
|
cannotGenerateMissingFields: 'Alternativtext kann nicht generiert werden. Erforderliche Felder fehlen.',
|
|
15
15
|
errorGeneratingAltText: 'Fehler beim Generieren des Alternativtextes. Bitte versuchen Sie es erneut.',
|
|
16
16
|
failedToGenerate: 'Generierung des Alternativtextes fehlgeschlagen. Bitte versuchen Sie es erneut.',
|
|
17
|
-
|
|
17
|
+
failedToGenerateForXImages_one: 'Generierung des Alternativtextes für {{count}} Bild fehlgeschlagen.',
|
|
18
|
+
failedToGenerateForXImages_other: 'Generierung des Alternativtextes für {{count}} Bilder fehlgeschlagen.',
|
|
18
19
|
noAltTextGenerated: 'Kein Alternativtext generiert. Bitte versuchen Sie es erneut.',
|
|
19
|
-
|
|
20
|
+
skippedNoAltTextNeeded_one: '{{count}} Datei übersprungen, die keinen Alternativtext benötigt.',
|
|
21
|
+
skippedNoAltTextNeeded_other: '{{count}} Dateien übersprungen, die keinen Alternativtext benötigen.',
|
|
22
|
+
skippedUnsupportedFormat_one: 'Für {{count}} Datei kann kein Alternativtext generiert werden. Bitte manuell ergänzen.',
|
|
23
|
+
skippedUnsupportedFormat_other: 'Für {{count}} Dateien kann kein Alternativtext generiert werden. Bitte manuell ergänzen.',
|
|
24
|
+
xOfYImagesUpdated_one: '{{updated}} von {{total}} Bild aktualisiert.',
|
|
25
|
+
xOfYImagesUpdated_other: '{{updated}} von {{total}} Bildern aktualisiert.',
|
|
20
26
|
// Help text
|
|
21
27
|
altTextDescription: 'Alternativtext für das Bild. Dieser wird für Screenreader und SEO verwendet. Er sollte die folgenden Anforderungen erfüllen:',
|
|
22
28
|
altTextRequirement1: 'Beschreibt in 1-2 Sätzen, was auf dem Bild zu sehen ist.',
|
|
@@ -32,11 +38,13 @@ export const de = {
|
|
|
32
38
|
altTextHealthWidget: 'Alternativtexte',
|
|
33
39
|
collectionCheckFailed: 'Status nicht verfügbar',
|
|
34
40
|
healthCheckPartialWarning: 'Einige Sammlungen konnten gerade nicht geprüft werden.',
|
|
35
|
-
|
|
41
|
+
localeCount_one: '{{count}} Sprache',
|
|
42
|
+
localeCount_other: '{{count}} Sprachen',
|
|
36
43
|
noImagesFound: 'In den konfigurierten Sammlungen wurden noch keine Bilder gefunden.',
|
|
37
44
|
statusHealthy: 'Alle vorhanden',
|
|
38
45
|
statusUnhealthy: '{{count}} fehlend',
|
|
39
|
-
|
|
46
|
+
totalImageCount_one: '{{count}} Bild',
|
|
47
|
+
totalImageCount_other: '{{count}} Bilder'
|
|
40
48
|
}
|
|
41
49
|
};
|
|
42
50
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/translations/de.ts"],"sourcesContent":["import type { GenericTranslationsObject } from './index.js'\n\nexport const de: GenericTranslationsObject = {\n $schema: './translation-schema.json',\n '@jhb.software/payload-alt-text-plugin': {\n // Field labels\n alternateText: 'Alternativtext',\n keywords: 'Schlüsselwörter',\n keywordsDescription:\n 'Schlüsselwörter, die das Bild beschreiben. Wird bei der Suche nach dem Bild verwendet.',\n\n // Button labels\n generateAltText: 'Alternativtext generieren',\n generateAltTextFor_one: 'Alternativtext für {{count}} Bild generieren',\n generateAltTextFor_other: 'Alternativtext für {{count}} Bilder generieren',\n\n // Toast messages\n altTextGeneratedSuccess:\n 'Alternativtext erfolgreich generiert. Bitte überprüfen und speichern Sie das Dokument.',\n cannotGenerateMissingFields:\n 'Alternativtext kann nicht generiert werden. Erforderliche Felder fehlen.',\n errorGeneratingAltText:\n 'Fehler beim Generieren des Alternativtextes. Bitte versuchen Sie es erneut.',\n failedToGenerate:\n 'Generierung des Alternativtextes fehlgeschlagen. Bitte versuchen Sie es erneut.',\n
|
|
1
|
+
{"version":3,"sources":["../../src/translations/de.ts"],"sourcesContent":["import type { GenericTranslationsObject } from './index.js'\n\nexport const de: GenericTranslationsObject = {\n $schema: './translation-schema.json',\n '@jhb.software/payload-alt-text-plugin': {\n // Field labels\n alternateText: 'Alternativtext',\n keywords: 'Schlüsselwörter',\n keywordsDescription:\n 'Schlüsselwörter, die das Bild beschreiben. Wird bei der Suche nach dem Bild verwendet.',\n\n // Button labels\n generateAltText: 'Alternativtext generieren',\n generateAltTextFor_one: 'Alternativtext für {{count}} Bild generieren',\n generateAltTextFor_other: 'Alternativtext für {{count}} Bilder generieren',\n\n // Toast messages\n altTextGeneratedSuccess:\n 'Alternativtext erfolgreich generiert. Bitte überprüfen und speichern Sie das Dokument.',\n cannotGenerateMissingFields:\n 'Alternativtext kann nicht generiert werden. Erforderliche Felder fehlen.',\n errorGeneratingAltText:\n 'Fehler beim Generieren des Alternativtextes. Bitte versuchen Sie es erneut.',\n failedToGenerate:\n 'Generierung des Alternativtextes fehlgeschlagen. Bitte versuchen Sie es erneut.',\n failedToGenerateForXImages_one:\n 'Generierung des Alternativtextes für {{count}} Bild fehlgeschlagen.',\n failedToGenerateForXImages_other:\n 'Generierung des Alternativtextes für {{count}} Bilder fehlgeschlagen.',\n noAltTextGenerated: 'Kein Alternativtext generiert. Bitte versuchen Sie es erneut.',\n skippedNoAltTextNeeded_one: '{{count}} Datei übersprungen, die keinen Alternativtext benötigt.',\n skippedNoAltTextNeeded_other:\n '{{count}} Dateien übersprungen, die keinen Alternativtext benötigen.',\n skippedUnsupportedFormat_one:\n 'Für {{count}} Datei kann kein Alternativtext generiert werden. Bitte manuell ergänzen.',\n skippedUnsupportedFormat_other:\n 'Für {{count}} Dateien kann kein Alternativtext generiert werden. Bitte manuell ergänzen.',\n xOfYImagesUpdated_one: '{{updated}} von {{total}} Bild aktualisiert.',\n xOfYImagesUpdated_other: '{{updated}} von {{total}} Bildern aktualisiert.',\n\n // Help text\n altTextDescription:\n 'Alternativtext für das Bild. Dieser wird für Screenreader und SEO verwendet. Er sollte die folgenden Anforderungen erfüllen:',\n altTextRequirement1: 'Beschreibt in 1-2 Sätzen, was auf dem Bild zu sehen ist.',\n altTextRequirement2:\n 'Sollte möglichst die gleichen Informationen oder den gleichen Zweck wie das Bild vermitteln.',\n altTextRequirement3:\n 'Phrasen wie \"Bild von\" oder \"Foto von\" sind überflüssig, da Screenreader bereits anzeigen, dass es sich um ein Bild handelt.',\n\n // Tooltips\n pleaseSaveDocumentFirst: 'Bitte speichern Sie zuerst das Dokument',\n unsupportedMimeType:\n 'Alternativtext-Generierung wird für {{mimeType}}-Dateien nicht unterstützt',\n\n // Validation messages\n theAlternateTextIsRequired: 'Der Alternativtext ist erforderlich.',\n\n // Dashboard widget\n altTextHealthDescription: 'Alternativtext Abdeckung in allen Upload-Sammlungen.',\n altTextHealthWidget: 'Alternativtexte',\n collectionCheckFailed: 'Status nicht verfügbar',\n healthCheckPartialWarning: 'Einige Sammlungen konnten gerade nicht geprüft werden.',\n localeCount_one: '{{count}} Sprache',\n localeCount_other: '{{count}} Sprachen',\n noImagesFound: 'In den konfigurierten Sammlungen wurden noch keine Bilder gefunden.',\n statusHealthy: 'Alle vorhanden',\n statusUnhealthy: '{{count}} fehlend',\n totalImageCount_one: '{{count}} Bild',\n totalImageCount_other: '{{count}} Bilder',\n },\n}\n"],"names":["de","$schema","alternateText","keywords","keywordsDescription","generateAltText","generateAltTextFor_one","generateAltTextFor_other","altTextGeneratedSuccess","cannotGenerateMissingFields","errorGeneratingAltText","failedToGenerate","failedToGenerateForXImages_one","failedToGenerateForXImages_other","noAltTextGenerated","skippedNoAltTextNeeded_one","skippedNoAltTextNeeded_other","skippedUnsupportedFormat_one","skippedUnsupportedFormat_other","xOfYImagesUpdated_one","xOfYImagesUpdated_other","altTextDescription","altTextRequirement1","altTextRequirement2","altTextRequirement3","pleaseSaveDocumentFirst","unsupportedMimeType","theAlternateTextIsRequired","altTextHealthDescription","altTextHealthWidget","collectionCheckFailed","healthCheckPartialWarning","localeCount_one","localeCount_other","noImagesFound","statusHealthy","statusUnhealthy","totalImageCount_one","totalImageCount_other"],"mappings":"AAEA,OAAO,MAAMA,KAAgC;IAC3CC,SAAS;IACT,yCAAyC;QACvC,eAAe;QACfC,eAAe;QACfC,UAAU;QACVC,qBACE;QAEF,gBAAgB;QAChBC,iBAAiB;QACjBC,wBAAwB;QACxBC,0BAA0B;QAE1B,iBAAiB;QACjBC,yBACE;QACFC,6BACE;QACFC,wBACE;QACFC,kBACE;QACFC,gCACE;QACFC,kCACE;QACFC,oBAAoB;QACpBC,4BAA4B;QAC5BC,8BACE;QACFC,8BACE;QACFC,gCACE;QACFC,uBAAuB;QACvBC,yBAAyB;QAEzB,YAAY;QACZC,oBACE;QACFC,qBAAqB;QACrBC,qBACE;QACFC,qBACE;QAEF,WAAW;QACXC,yBAAyB;QACzBC,qBACE;QAEF,sBAAsB;QACtBC,4BAA4B;QAE5B,mBAAmB;QACnBC,0BAA0B;QAC1BC,qBAAqB;QACrBC,uBAAuB;QACvBC,2BAA2B;QAC3BC,iBAAiB;QACjBC,mBAAmB;QACnBC,eAAe;QACfC,eAAe;QACfC,iBAAiB;QACjBC,qBAAqB;QACrBC,uBAAuB;IACzB;AACF,EAAC"}
|
package/dist/translations/en.js
CHANGED
|
@@ -14,9 +14,15 @@ export const en = {
|
|
|
14
14
|
cannotGenerateMissingFields: 'Cannot generate alt text. Missing required fields.',
|
|
15
15
|
errorGeneratingAltText: 'Error generating alt text. Please try again.',
|
|
16
16
|
failedToGenerate: 'Failed to generate alt text. Please try again.',
|
|
17
|
-
|
|
17
|
+
failedToGenerateForXImages_one: 'Failed to generate alt text for {{count}} image.',
|
|
18
|
+
failedToGenerateForXImages_other: 'Failed to generate alt text for {{count}} images.',
|
|
18
19
|
noAltTextGenerated: 'No alt text generated. Please try again.',
|
|
19
|
-
|
|
20
|
+
skippedNoAltTextNeeded_one: 'Skipped {{count}} file that does not need alt text.',
|
|
21
|
+
skippedNoAltTextNeeded_other: 'Skipped {{count}} files that do not need alt text.',
|
|
22
|
+
skippedUnsupportedFormat_one: 'Alt text cannot be generated for {{count}} file. Please write it by hand.',
|
|
23
|
+
skippedUnsupportedFormat_other: 'Alt text cannot be generated for {{count}} files. Please write it by hand.',
|
|
24
|
+
xOfYImagesUpdated_one: '{{updated}} of {{total}} image updated.',
|
|
25
|
+
xOfYImagesUpdated_other: '{{updated}} of {{total}} images updated.',
|
|
20
26
|
// Help text
|
|
21
27
|
altTextDescription: 'Alternate text for the image. This will be used for screen readers and SEO. It should meet the following requirements:',
|
|
22
28
|
altTextRequirement1: 'Describes in 1-2 sentences, what is visible in the image.',
|
|
@@ -32,11 +38,13 @@ export const en = {
|
|
|
32
38
|
altTextHealthWidget: 'Alt Texts',
|
|
33
39
|
collectionCheckFailed: 'Status unavailable',
|
|
34
40
|
healthCheckPartialWarning: 'Some collections could not be checked right now.',
|
|
35
|
-
|
|
41
|
+
localeCount_one: '{{count}} locale',
|
|
42
|
+
localeCount_other: '{{count}} locales',
|
|
36
43
|
noImagesFound: 'No images found in the configured collections yet.',
|
|
37
44
|
statusHealthy: 'All set',
|
|
38
45
|
statusUnhealthy: '{{count}} missing',
|
|
39
|
-
|
|
46
|
+
totalImageCount_one: '{{count}} image',
|
|
47
|
+
totalImageCount_other: '{{count}} images'
|
|
40
48
|
}
|
|
41
49
|
};
|
|
42
50
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/translations/en.ts"],"sourcesContent":["import type { GenericTranslationsObject } from './index.js'\n\nexport const en: GenericTranslationsObject = {\n $schema: './translation-schema.json',\n '@jhb.software/payload-alt-text-plugin': {\n // Field labels\n alternateText: 'Alternate text',\n keywords: 'Keywords',\n keywordsDescription: 'Keywords which describe the image. Used when searching for the image.',\n\n // Button labels\n generateAltText: 'Generate alt text',\n generateAltTextFor_one: 'Generate alt text for {{count}} image',\n generateAltTextFor_other: 'Generate alt text for {{count}} images',\n\n // Toast messages\n altTextGeneratedSuccess:\n 'Alt text generated successfully. Please review and save the document.',\n cannotGenerateMissingFields: 'Cannot generate alt text. Missing required fields.',\n errorGeneratingAltText: 'Error generating alt text. Please try again.',\n failedToGenerate: 'Failed to generate alt text. Please try again.',\n
|
|
1
|
+
{"version":3,"sources":["../../src/translations/en.ts"],"sourcesContent":["import type { GenericTranslationsObject } from './index.js'\n\nexport const en: GenericTranslationsObject = {\n $schema: './translation-schema.json',\n '@jhb.software/payload-alt-text-plugin': {\n // Field labels\n alternateText: 'Alternate text',\n keywords: 'Keywords',\n keywordsDescription: 'Keywords which describe the image. Used when searching for the image.',\n\n // Button labels\n generateAltText: 'Generate alt text',\n generateAltTextFor_one: 'Generate alt text for {{count}} image',\n generateAltTextFor_other: 'Generate alt text for {{count}} images',\n\n // Toast messages\n altTextGeneratedSuccess:\n 'Alt text generated successfully. Please review and save the document.',\n cannotGenerateMissingFields: 'Cannot generate alt text. Missing required fields.',\n errorGeneratingAltText: 'Error generating alt text. Please try again.',\n failedToGenerate: 'Failed to generate alt text. Please try again.',\n failedToGenerateForXImages_one: 'Failed to generate alt text for {{count}} image.',\n failedToGenerateForXImages_other: 'Failed to generate alt text for {{count}} images.',\n noAltTextGenerated: 'No alt text generated. Please try again.',\n skippedNoAltTextNeeded_one: 'Skipped {{count}} file that does not need alt text.',\n skippedNoAltTextNeeded_other: 'Skipped {{count}} files that do not need alt text.',\n skippedUnsupportedFormat_one:\n 'Alt text cannot be generated for {{count}} file. Please write it by hand.',\n skippedUnsupportedFormat_other:\n 'Alt text cannot be generated for {{count}} files. Please write it by hand.',\n xOfYImagesUpdated_one: '{{updated}} of {{total}} image updated.',\n xOfYImagesUpdated_other: '{{updated}} of {{total}} images updated.',\n\n // Help text\n altTextDescription:\n 'Alternate text for the image. This will be used for screen readers and SEO. It should meet the following requirements:',\n altTextRequirement1: 'Describes in 1-2 sentences, what is visible in the image.',\n altTextRequirement2:\n 'Should convey the same information or purpose as the image, whenever possible.',\n altTextRequirement3:\n 'Phrases like \"image of\" or \"picture of\" are unnecessary, since screen readers already announce that it\\'s an image.',\n\n // Tooltips\n pleaseSaveDocumentFirst: 'Please save the document first',\n unsupportedMimeType: 'Alt text generation is not supported for {{mimeType}} files',\n\n // Validation messages\n theAlternateTextIsRequired: 'An alternate text is required.',\n\n // Dashboard widget\n altTextHealthDescription: 'Alt text coverage across upload collections.',\n altTextHealthWidget: 'Alt Texts',\n collectionCheckFailed: 'Status unavailable',\n healthCheckPartialWarning: 'Some collections could not be checked right now.',\n localeCount_one: '{{count}} locale',\n localeCount_other: '{{count}} locales',\n noImagesFound: 'No images found in the configured collections yet.',\n statusHealthy: 'All set',\n statusUnhealthy: '{{count}} missing',\n totalImageCount_one: '{{count}} image',\n totalImageCount_other: '{{count}} images',\n },\n}\n"],"names":["en","$schema","alternateText","keywords","keywordsDescription","generateAltText","generateAltTextFor_one","generateAltTextFor_other","altTextGeneratedSuccess","cannotGenerateMissingFields","errorGeneratingAltText","failedToGenerate","failedToGenerateForXImages_one","failedToGenerateForXImages_other","noAltTextGenerated","skippedNoAltTextNeeded_one","skippedNoAltTextNeeded_other","skippedUnsupportedFormat_one","skippedUnsupportedFormat_other","xOfYImagesUpdated_one","xOfYImagesUpdated_other","altTextDescription","altTextRequirement1","altTextRequirement2","altTextRequirement3","pleaseSaveDocumentFirst","unsupportedMimeType","theAlternateTextIsRequired","altTextHealthDescription","altTextHealthWidget","collectionCheckFailed","healthCheckPartialWarning","localeCount_one","localeCount_other","noImagesFound","statusHealthy","statusUnhealthy","totalImageCount_one","totalImageCount_other"],"mappings":"AAEA,OAAO,MAAMA,KAAgC;IAC3CC,SAAS;IACT,yCAAyC;QACvC,eAAe;QACfC,eAAe;QACfC,UAAU;QACVC,qBAAqB;QAErB,gBAAgB;QAChBC,iBAAiB;QACjBC,wBAAwB;QACxBC,0BAA0B;QAE1B,iBAAiB;QACjBC,yBACE;QACFC,6BAA6B;QAC7BC,wBAAwB;QACxBC,kBAAkB;QAClBC,gCAAgC;QAChCC,kCAAkC;QAClCC,oBAAoB;QACpBC,4BAA4B;QAC5BC,8BAA8B;QAC9BC,8BACE;QACFC,gCACE;QACFC,uBAAuB;QACvBC,yBAAyB;QAEzB,YAAY;QACZC,oBACE;QACFC,qBAAqB;QACrBC,qBACE;QACFC,qBACE;QAEF,WAAW;QACXC,yBAAyB;QACzBC,qBAAqB;QAErB,sBAAsB;QACtBC,4BAA4B;QAE5B,mBAAmB;QACnBC,0BAA0B;QAC1BC,qBAAqB;QACrBC,uBAAuB;QACvBC,2BAA2B;QAC3BC,iBAAiB;QACjBC,mBAAmB;QACnBC,eAAe;QACfC,eAAe;QACfC,iBAAiB;QACjBC,qBAAqB;QACrBC,uBAAuB;IACzB;AACF,EAAC"}
|
|
@@ -17,22 +17,30 @@
|
|
|
17
17
|
"collectionCheckFailed": { "type": "string" },
|
|
18
18
|
"errorGeneratingAltText": { "type": "string" },
|
|
19
19
|
"failedToGenerate": { "type": "string" },
|
|
20
|
-
"
|
|
20
|
+
"failedToGenerateForXImages_one": { "type": "string" },
|
|
21
|
+
"failedToGenerateForXImages_other": { "type": "string" },
|
|
21
22
|
"generateAltText": { "type": "string" },
|
|
22
23
|
"generateAltTextFor_one": { "type": "string" },
|
|
23
24
|
"generateAltTextFor_other": { "type": "string" },
|
|
24
25
|
"healthCheckPartialWarning": { "type": "string" },
|
|
25
26
|
"keywords": { "type": "string" },
|
|
26
|
-
"
|
|
27
|
+
"localeCount_one": { "type": "string" },
|
|
28
|
+
"localeCount_other": { "type": "string" },
|
|
27
29
|
"keywordsDescription": { "type": "string" },
|
|
28
30
|
"noAltTextGenerated": { "type": "string" },
|
|
29
31
|
"noImagesFound": { "type": "string" },
|
|
30
32
|
"pleaseSaveDocumentFirst": { "type": "string" },
|
|
33
|
+
"skippedNoAltTextNeeded_one": { "type": "string" },
|
|
34
|
+
"skippedNoAltTextNeeded_other": { "type": "string" },
|
|
35
|
+
"skippedUnsupportedFormat_one": { "type": "string" },
|
|
36
|
+
"skippedUnsupportedFormat_other": { "type": "string" },
|
|
31
37
|
"statusHealthy": { "type": "string" },
|
|
32
38
|
"statusUnhealthy": { "type": "string" },
|
|
33
39
|
"theAlternateTextIsRequired": { "type": "string" },
|
|
34
|
-
"
|
|
35
|
-
"
|
|
40
|
+
"totalImageCount_one": { "type": "string" },
|
|
41
|
+
"totalImageCount_other": { "type": "string" },
|
|
42
|
+
"xOfYImagesUpdated_one": { "type": "string" },
|
|
43
|
+
"xOfYImagesUpdated_other": { "type": "string" }
|
|
36
44
|
},
|
|
37
45
|
"required": [
|
|
38
46
|
"alternateText",
|
|
@@ -47,22 +55,30 @@
|
|
|
47
55
|
"collectionCheckFailed",
|
|
48
56
|
"errorGeneratingAltText",
|
|
49
57
|
"failedToGenerate",
|
|
50
|
-
"
|
|
58
|
+
"failedToGenerateForXImages_one",
|
|
59
|
+
"failedToGenerateForXImages_other",
|
|
51
60
|
"generateAltText",
|
|
52
61
|
"generateAltTextFor_one",
|
|
53
62
|
"generateAltTextFor_other",
|
|
54
63
|
"healthCheckPartialWarning",
|
|
55
64
|
"keywords",
|
|
56
65
|
"keywordsDescription",
|
|
57
|
-
"
|
|
66
|
+
"localeCount_one",
|
|
67
|
+
"localeCount_other",
|
|
58
68
|
"noAltTextGenerated",
|
|
59
69
|
"noImagesFound",
|
|
60
70
|
"pleaseSaveDocumentFirst",
|
|
71
|
+
"skippedNoAltTextNeeded_one",
|
|
72
|
+
"skippedNoAltTextNeeded_other",
|
|
73
|
+
"skippedUnsupportedFormat_one",
|
|
74
|
+
"skippedUnsupportedFormat_other",
|
|
61
75
|
"statusHealthy",
|
|
62
76
|
"statusUnhealthy",
|
|
63
77
|
"theAlternateTextIsRequired",
|
|
64
|
-
"
|
|
65
|
-
"
|
|
78
|
+
"totalImageCount_one",
|
|
79
|
+
"totalImageCount_other",
|
|
80
|
+
"xOfYImagesUpdated_one",
|
|
81
|
+
"xOfYImagesUpdated_other"
|
|
66
82
|
]
|
|
67
83
|
}
|
|
68
84
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CollectionSlug, 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 };
|
|
@@ -18,6 +18,61 @@ export type GetImageThumbnail = (doc: Record<string, unknown>, args: {
|
|
|
18
18
|
collection: CollectionSlug;
|
|
19
19
|
req: PayloadRequest;
|
|
20
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
|
+
/**
|
|
40
|
+
* Narrows the locales a request generates for and is measured against — in a
|
|
41
|
+
* multi-tenant CMS, to the locales of the tenant the request is for.
|
|
42
|
+
*
|
|
43
|
+
* Must return a non-empty subset of `locales`; anything else ends the operation
|
|
44
|
+
* with an error rather than writing into a locale the project does not define.
|
|
45
|
+
*
|
|
46
|
+
* @param args.locales The locales configured on the Payload config.
|
|
47
|
+
* @param args.req The request being served.
|
|
48
|
+
*/
|
|
49
|
+
export type FilterLocales = (args: {
|
|
50
|
+
locales: string[];
|
|
51
|
+
req: PayloadRequest;
|
|
52
|
+
}) => Promise<string[]> | string[];
|
|
53
|
+
/** Configuration of the alt text health feature. */
|
|
54
|
+
export type AltTextHealthCheckConfig = {
|
|
55
|
+
/**
|
|
56
|
+
* Access control for the health report — both the REST endpoint and the dashboard
|
|
57
|
+
* widget, which hides itself when this denies.
|
|
58
|
+
*
|
|
59
|
+
* Use it to restrict the collection-wide report more strictly than the
|
|
60
|
+
* per-document generate endpoints (e.g. to admins).
|
|
61
|
+
*
|
|
62
|
+
* @default the plugin's `access`
|
|
63
|
+
*/
|
|
64
|
+
access?: (args: {
|
|
65
|
+
req: PayloadRequest;
|
|
66
|
+
}) => boolean | Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Narrows what the report counts. See {@link AltTextHealthBaseFilter}.
|
|
69
|
+
*
|
|
70
|
+
* This is not access control: it scopes the aggregate, it does not decide who
|
|
71
|
+
* may see it. Use `access` for that, and note the report is always filtered to
|
|
72
|
+
* the collections the requesting user can read.
|
|
73
|
+
*/
|
|
74
|
+
baseFilter?: AltTextHealthBaseFilter;
|
|
75
|
+
};
|
|
21
76
|
/** Configuration options for the alt text plugin. */
|
|
22
77
|
export type IncomingAltTextPluginConfig = {
|
|
23
78
|
/**
|
|
@@ -51,6 +106,13 @@ export type IncomingAltTextPluginConfig = {
|
|
|
51
106
|
fieldsOverride?: (args: {
|
|
52
107
|
defaultFields: Field[];
|
|
53
108
|
}) => Field[];
|
|
109
|
+
/**
|
|
110
|
+
* Narrows the locales this request targets. See {@link FilterLocales}.
|
|
111
|
+
*
|
|
112
|
+
* Governs bulk generation, the generate endpoint's locale validation, and the
|
|
113
|
+
* health report.
|
|
114
|
+
*/
|
|
115
|
+
filterLocales?: FilterLocales;
|
|
54
116
|
/**
|
|
55
117
|
* Builds the image URL sent to the resolver. See {@link GetImageThumbnail}.
|
|
56
118
|
*
|
|
@@ -65,19 +127,12 @@ export type IncomingAltTextPluginConfig = {
|
|
|
65
127
|
* Controls the alt text health feature (REST endpoint, cache revalidation hooks, and dashboard widget).
|
|
66
128
|
*
|
|
67
129
|
* - `false` disables the entire feature.
|
|
68
|
-
* - `true` enables it, gated by `access
|
|
69
|
-
* -
|
|
70
|
-
* with that access check — use this to restrict the collection-wide report
|
|
71
|
-
* more strictly than the per-document generate endpoints (e.g. to admins).
|
|
72
|
-
*
|
|
73
|
-
* Regardless of the gate, the report is always filtered to the collections the
|
|
74
|
-
* requesting user can read.
|
|
130
|
+
* - `true` enables it, gated by `access` and covering every document.
|
|
131
|
+
* - An object enables it and configures it. See {@link AltTextHealthCheckConfig}.
|
|
75
132
|
*
|
|
76
133
|
* @default true
|
|
77
134
|
*/
|
|
78
|
-
healthCheck?:
|
|
79
|
-
req: PayloadRequest;
|
|
80
|
-
}) => boolean | Promise<boolean>) | boolean;
|
|
135
|
+
healthCheck?: AltTextHealthCheckConfig | boolean;
|
|
81
136
|
/**
|
|
82
137
|
* The MIME type `getImageThumbnail` delivers, for every configured collection.
|
|
83
138
|
*
|
|
@@ -133,6 +188,8 @@ export type AltTextPluginConfig = {
|
|
|
133
188
|
fieldsOverride?: (args: {
|
|
134
189
|
defaultFields: Field[];
|
|
135
190
|
}) => Field[];
|
|
191
|
+
/** Narrows the locales one request targets. See {@link FilterLocales}. */
|
|
192
|
+
filterLocales?: FilterLocales;
|
|
136
193
|
/** Function to get the thumbnail URL of an image document. */
|
|
137
194
|
getImageThumbnail: GetImageThumbnail;
|
|
138
195
|
/** Whether alt text health tracking is enabled. */
|
|
@@ -141,9 +198,11 @@ export type AltTextPluginConfig = {
|
|
|
141
198
|
healthCheckAccess: (args: {
|
|
142
199
|
req: PayloadRequest;
|
|
143
200
|
}) => boolean | Promise<boolean>;
|
|
201
|
+
/** Narrows what the health report counts. See {@link AltTextHealthBaseFilter}. */
|
|
202
|
+
healthCheckBaseFilter?: AltTextHealthBaseFilter;
|
|
144
203
|
/** The locale to generate alt texts in when localization is disabled. */
|
|
145
204
|
locale?: string;
|
|
146
|
-
/** The locales
|
|
205
|
+
/** The locales configured on the Payload config. Empty when localization is disabled. */
|
|
147
206
|
locales: string[];
|
|
148
207
|
/** Maximum number of concurrent API requests for bulk generate operations. */
|
|
149
208
|
maxBulkGenerateConcurrency: number;
|