@jhb.software/payload-alt-text-plugin 0.10.0 → 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 +164 -18
- package/dist/endpoints/bulkGenerateAltTexts.js +8 -4
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +3 -1
- 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 +19 -4
- 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 +14 -1
- package/dist/resolvers/mistral.js +84 -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/types/AltTextPluginConfig.d.ts +47 -11
- 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/stableStringify.d.ts +9 -0
- package/dist/utilities/stableStringify.js +19 -0
- package/dist/utilities/stableStringify.js.map +1 -0
- package/package.json +4 -5
|
@@ -1,220 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createVisionResolver, VisionProviderError } from './createVisionResolver.js';
|
|
2
2
|
/**
|
|
3
3
|
* Image formats the Mistral API accepts.
|
|
4
4
|
*
|
|
5
5
|
* Narrower than what an upload collection may hold — SVG and AVIF are missing,
|
|
6
6
|
* so the endpoint rejects those documents and their generate button stays
|
|
7
7
|
* disabled instead of failing at the provider.
|
|
8
|
+
*
|
|
9
|
+
* @see https://docs.mistral.ai/capabilities/vision/
|
|
8
10
|
*/ const SUPPORTED_MIME_TYPES = [
|
|
9
11
|
'image/jpeg',
|
|
10
12
|
'image/png',
|
|
11
13
|
'image/gif',
|
|
12
14
|
'image/webp'
|
|
13
15
|
];
|
|
14
|
-
/** Mistral rejects images above 20 MB. */ const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
15
|
-
const altTextSchema = z.object({
|
|
16
|
-
altText: z.string().describe('A concise, descriptive alt text for the image'),
|
|
17
|
-
keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
|
|
18
|
-
});
|
|
19
|
-
/** One schema entry per requested locale, so the model must answer for all of them. */ const schemaForLocales = (locales)=>z.object(Object.fromEntries(locales.map((locale)=>[
|
|
20
|
-
locale,
|
|
21
|
-
altTextSchema
|
|
22
|
-
])));
|
|
23
|
-
/**
|
|
24
|
-
* Downloads the image and returns it as a data URI.
|
|
25
|
-
*
|
|
26
|
-
* Mistral can fetch an image URL itself, but that path is not dependable for a
|
|
27
|
-
* CMS. It requires the file to be reachable from the public internet — never
|
|
28
|
-
* true in local development, and not true for private buckets — and some hosts
|
|
29
|
-
* refuse Mistral's fetcher outright, which surfaces as `File could not be
|
|
30
|
-
* fetched from url` (error 3310). Sending the bytes removes that whole class of
|
|
31
|
-
* failure for the price of one extra download.
|
|
32
|
-
*/ async function fetchImageAsDataUri(url, signal) {
|
|
33
|
-
let response;
|
|
34
|
-
try {
|
|
35
|
-
response = await fetch(url, {
|
|
36
|
-
signal
|
|
37
|
-
});
|
|
38
|
-
} catch (error) {
|
|
39
|
-
return {
|
|
40
|
-
error: `Could not download the image from ${url}: ${error instanceof Error ? error.message : 'unknown error'}`
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
return {
|
|
45
|
-
error: `Could not download the image from ${url}: status ${response.status}`
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
// The document's mime type is checked by the endpoint before the resolver
|
|
49
|
-
// runs, but `getImageThumbnail` may point at a derivative in a different
|
|
50
|
-
// format, so trust what was actually served.
|
|
51
|
-
const contentType = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase();
|
|
52
|
-
if (!contentType || !SUPPORTED_MIME_TYPES.includes(contentType)) {
|
|
53
|
-
return {
|
|
54
|
-
error: `The image at ${url} was served as "${contentType ?? 'an unknown type'}", which Mistral cannot read. Supported types: ${SUPPORTED_MIME_TYPES.join(', ')}.`
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
const bytes = Buffer.from(await response.arrayBuffer());
|
|
58
|
-
if (bytes.byteLength === 0) {
|
|
59
|
-
return {
|
|
60
|
-
error: `The image at ${url} was empty.`
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
if (bytes.byteLength > MAX_IMAGE_BYTES) {
|
|
64
|
-
return {
|
|
65
|
-
error: `The image at ${url} is ${Math.round(bytes.byteLength / 1024 / 1024)} MB, above Mistral's 20 MB limit. Point getImageThumbnail at a smaller image size.`
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
dataUri: `data:${contentType};base64,${bytes.toString('base64')}`
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
function buildPrompt(locales) {
|
|
73
|
-
const languages = locales.join(', ');
|
|
74
|
-
return `
|
|
75
|
-
You are an expert at analyzing images and creating descriptive image alt text.
|
|
76
|
-
|
|
77
|
-
Please analyze the given image and provide the following in ${languages}:
|
|
78
|
-
- 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.
|
|
79
|
-
- A list of keywords that describe the content (e.g., ["Camel", "Palm trees", "Desert"]) as "keywords"
|
|
80
|
-
|
|
81
|
-
If a context is provided, use it to enhance the alt text.
|
|
82
|
-
|
|
83
|
-
Format your response as a JSON object with ${locales.map((locale)=>`"${locale}"`).join(', ')} keys, each containing "altText" and "keywords".
|
|
84
|
-
`;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Reads the model's response.
|
|
88
|
-
*
|
|
89
|
-
* Deliberately strict about a blank `altText`: the field is required on the
|
|
90
|
-
* collection, so an empty string would satisfy that requirement while telling a
|
|
91
|
-
* screen reader nothing — and nobody looks at an alt text again once it is set.
|
|
92
|
-
*/ function parseResults(content, locales) {
|
|
93
|
-
const parsed = schemaForLocales(locales).safeParse(content);
|
|
94
|
-
if (!parsed.success) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
const results = {};
|
|
98
|
-
for (const locale of locales){
|
|
99
|
-
const entry = parsed.data[locale];
|
|
100
|
-
if (entry.altText.trim().length === 0) {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
results[locale] = {
|
|
104
|
-
altText: entry.altText.trim(),
|
|
105
|
-
keywords: entry.keywords
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
return results;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* One request, one or more locales.
|
|
112
|
-
*
|
|
113
|
-
* All locales go into a single call rather than one call each: the image is
|
|
114
|
-
* uploaded and analyzed once — the expensive part — and every language ends up
|
|
115
|
-
* describing the same reading of it.
|
|
116
|
-
*/ async function generate({ apiKey, baseUrl, filename, imageThumbnailUrl, locales, model, timeoutMs }) {
|
|
117
|
-
if (!apiKey) {
|
|
118
|
-
return {
|
|
119
|
-
error: 'No Mistral API key configured',
|
|
120
|
-
success: false
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
if (locales.length === 0) {
|
|
124
|
-
return {
|
|
125
|
-
error: 'No locale requested',
|
|
126
|
-
success: false
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
const signal = AbortSignal.timeout(timeoutMs);
|
|
130
|
-
const image = await fetchImageAsDataUri(imageThumbnailUrl, signal);
|
|
131
|
-
if ('error' in image) {
|
|
132
|
-
return {
|
|
133
|
-
error: image.error,
|
|
134
|
-
success: false
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
139
|
-
body: JSON.stringify({
|
|
140
|
-
max_tokens: 150 * locales.length,
|
|
141
|
-
messages: [
|
|
142
|
-
{
|
|
143
|
-
content: buildPrompt(locales),
|
|
144
|
-
role: 'system'
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
content: [
|
|
148
|
-
{
|
|
149
|
-
type: 'image_url',
|
|
150
|
-
image_url: image.dataUri
|
|
151
|
-
},
|
|
152
|
-
...filename ? [
|
|
153
|
-
{
|
|
154
|
-
type: 'text',
|
|
155
|
-
text: filename
|
|
156
|
-
}
|
|
157
|
-
] : []
|
|
158
|
-
],
|
|
159
|
-
role: 'user'
|
|
160
|
-
}
|
|
161
|
-
],
|
|
162
|
-
model,
|
|
163
|
-
response_format: {
|
|
164
|
-
type: 'json_schema',
|
|
165
|
-
json_schema: {
|
|
166
|
-
name: 'data',
|
|
167
|
-
schema: z.toJSONSchema(schemaForLocales(locales), {
|
|
168
|
-
target: 'draft-7'
|
|
169
|
-
}),
|
|
170
|
-
strict: true
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}),
|
|
174
|
-
headers: {
|
|
175
|
-
Authorization: `Bearer ${apiKey}`,
|
|
176
|
-
'Content-Type': 'application/json'
|
|
177
|
-
},
|
|
178
|
-
method: 'POST',
|
|
179
|
-
signal
|
|
180
|
-
});
|
|
181
|
-
if (!response.ok) {
|
|
182
|
-
const body = await response.text().catch(()=>'');
|
|
183
|
-
return {
|
|
184
|
-
error: `Mistral responded with status ${response.status}${body ? `: ${body}` : ''}`,
|
|
185
|
-
success: false
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
const completion = await response.json();
|
|
189
|
-
const content = completion.choices?.[0]?.message?.content;
|
|
190
|
-
if (typeof content !== 'string') {
|
|
191
|
-
return {
|
|
192
|
-
error: 'No result from Mistral',
|
|
193
|
-
success: false
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
const results = parseResults(JSON.parse(content), locales);
|
|
197
|
-
if (!results) {
|
|
198
|
-
return {
|
|
199
|
-
error: `Mistral did not return a usable alt text for every requested locale (${locales.join(', ')})`,
|
|
200
|
-
success: false
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
return {
|
|
204
|
-
results,
|
|
205
|
-
success: true
|
|
206
|
-
};
|
|
207
|
-
} catch (error) {
|
|
208
|
-
console.error('Error generating alt text:', error);
|
|
209
|
-
return {
|
|
210
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
211
|
-
success: false
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
16
|
/**
|
|
216
17
|
* Creates a Mistral-based resolver for alt text generation.
|
|
217
18
|
*
|
|
19
|
+
* The image is downloaded and sent as bytes rather than handed to Mistral as a
|
|
20
|
+
* URL. Mistral's own fetcher requires a publicly reachable file — never true in
|
|
21
|
+
* local development, and not true for private buckets — and some hosts refuse it
|
|
22
|
+
* outright, which surfaces as `File could not be fetched from url` (error 3310).
|
|
23
|
+
*
|
|
218
24
|
* @example
|
|
219
25
|
* ```typescript
|
|
220
26
|
* import { mistralResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
@@ -224,57 +30,85 @@ function buildPrompt(locales) {
|
|
|
224
30
|
* model: 'mistral-medium-latest', // optional, this is the default
|
|
225
31
|
* })
|
|
226
32
|
* ```
|
|
227
|
-
*/ export const mistralResolver = (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const result = await generate({
|
|
233
|
-
apiKey,
|
|
234
|
-
baseUrl,
|
|
235
|
-
filename,
|
|
236
|
-
imageThumbnailUrl,
|
|
237
|
-
locales: [
|
|
238
|
-
locale
|
|
239
|
-
],
|
|
240
|
-
model,
|
|
241
|
-
timeoutMs
|
|
242
|
-
});
|
|
243
|
-
if (!result.success) {
|
|
244
|
-
return {
|
|
245
|
-
error: result.error,
|
|
246
|
-
success: false
|
|
247
|
-
};
|
|
33
|
+
*/ export const mistralResolver = ({ apiKey, baseUrl = 'https://api.mistral.ai/v1', instructions, model = 'mistral-medium-latest', timeoutMs = 30_000 })=>createVisionResolver({
|
|
34
|
+
apiKey,
|
|
35
|
+
generate: async ({ filename, image, instructions: resolvedInstructions, maxTokens, responseSchema, signal })=>{
|
|
36
|
+
if (!image) {
|
|
37
|
+
throw new Error('The image was not downloaded');
|
|
248
38
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
39
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
max_tokens: maxTokens,
|
|
42
|
+
messages: [
|
|
43
|
+
{
|
|
44
|
+
content: resolvedInstructions,
|
|
45
|
+
role: 'system'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: 'image_url',
|
|
51
|
+
image_url: image.dataUri
|
|
52
|
+
},
|
|
53
|
+
...filename ? [
|
|
54
|
+
{
|
|
55
|
+
type: 'text',
|
|
56
|
+
text: filename
|
|
57
|
+
}
|
|
58
|
+
] : []
|
|
59
|
+
],
|
|
60
|
+
role: 'user'
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
model,
|
|
64
|
+
response_format: {
|
|
65
|
+
type: 'json_schema',
|
|
66
|
+
json_schema: {
|
|
67
|
+
name: 'data',
|
|
68
|
+
schema: responseSchema,
|
|
69
|
+
strict: true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}),
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${apiKey}`,
|
|
75
|
+
'Content-Type': 'application/json'
|
|
76
|
+
},
|
|
77
|
+
method: 'POST',
|
|
78
|
+
signal
|
|
263
79
|
});
|
|
264
|
-
if (!
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
// Bounded: unbounded provider text would land in the log as-is.
|
|
82
|
+
const body = (await response.text().catch(()=>'')).slice(0, 500);
|
|
83
|
+
throw new VisionProviderError({
|
|
84
|
+
body,
|
|
85
|
+
label: 'Mistral',
|
|
86
|
+
status: response.status
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const completion = await response.json();
|
|
90
|
+
const choice = completion.choices?.[0];
|
|
91
|
+
if (choice?.finish_reason === 'length') {
|
|
92
|
+
throw new Error(`Mistral ran out of tokens before finishing the alt text (max_tokens: ${maxTokens})`);
|
|
93
|
+
}
|
|
94
|
+
const content = choice?.message?.content;
|
|
95
|
+
if (typeof content !== 'string') {
|
|
96
|
+
throw new Error('No result from Mistral');
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(content);
|
|
100
|
+
} catch {
|
|
101
|
+
throw new Error('Mistral returned a response that was not valid JSON');
|
|
269
102
|
}
|
|
270
|
-
return {
|
|
271
|
-
results: result.results,
|
|
272
|
-
success: true
|
|
273
|
-
};
|
|
274
103
|
},
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
104
|
+
inlineImage: true,
|
|
105
|
+
instructions,
|
|
106
|
+
key: 'mistral',
|
|
107
|
+
label: 'Mistral',
|
|
108
|
+
// Mistral rejects images above 20 MB.
|
|
109
|
+
maxImageBytes: 20 * 1024 * 1024,
|
|
110
|
+
supportedMimeTypes: SUPPORTED_MIME_TYPES,
|
|
111
|
+
timeoutMs
|
|
112
|
+
});
|
|
279
113
|
|
|
280
114
|
//# sourceMappingURL=mistral.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/resolvers/mistral.ts"],"sourcesContent":["import { z } from 'zod'\n\nimport type {\n AltTextBulkResolverArgs,\n AltTextBulkResolverResponse,\n AltTextResolver,\n AltTextResolverArgs,\n AltTextResolverResponse,\n AltTextResult,\n} from './types.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 * 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 */\nconst SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']\n\n/** Mistral rejects images above 20 MB. */\nconst MAX_IMAGE_BYTES = 20 * 1024 * 1024\n\nconst altTextSchema = 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/** One schema entry per requested locale, so the model must answer for all of them. */\nconst schemaForLocales = (locales: string[]) =>\n z.object(Object.fromEntries(locales.map((locale) => [locale, altTextSchema])))\n\n/**\n * Downloads the image and returns it as a data URI.\n *\n * Mistral can fetch an image URL itself, but that path is not dependable for a\n * CMS. It requires the file to be reachable from the public internet — never\n * true in local development, and not true for private buckets — and some hosts\n * refuse Mistral's fetcher outright, which surfaces as `File could not be\n * fetched from url` (error 3310). Sending the bytes removes that whole class of\n * failure for the price of one extra download.\n */\nasync function fetchImageAsDataUri(\n url: string,\n signal: AbortSignal,\n): Promise<{ dataUri: string } | { error: string }> {\n let response: Response\n\n try {\n response = await fetch(url, { signal })\n } catch (error) {\n return {\n error: `Could not download the image from ${url}: ${error instanceof Error ? error.message : 'unknown error'}`,\n }\n }\n\n if (!response.ok) {\n return { error: `Could not download the image from ${url}: status ${response.status}` }\n }\n\n // The document's mime type is checked by the endpoint before the resolver\n // runs, but `getImageThumbnail` may point at a derivative in a different\n // format, so trust what was actually served.\n const contentType = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase()\n\n if (!contentType || !SUPPORTED_MIME_TYPES.includes(contentType)) {\n return {\n error: `The image at ${url} was served as \"${contentType ?? 'an unknown type'}\", which Mistral cannot read. Supported types: ${SUPPORTED_MIME_TYPES.join(', ')}.`,\n }\n }\n\n const bytes = Buffer.from(await response.arrayBuffer())\n\n if (bytes.byteLength === 0) {\n return { error: `The image at ${url} was empty.` }\n }\n\n if (bytes.byteLength > MAX_IMAGE_BYTES) {\n return {\n error: `The image at ${url} is ${Math.round(bytes.byteLength / 1024 / 1024)} MB, above Mistral's 20 MB limit. Point getImageThumbnail at a smaller image size.`,\n }\n }\n\n return { dataUri: `data:${contentType};base64,${bytes.toString('base64')}` }\n}\n\nfunction buildPrompt(locales: string[]): string {\n const languages = locales.join(', ')\n\n return `\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 ${languages}:\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 with ${locales.map((locale) => `\"${locale}\"`).join(', ')} keys, each containing \"altText\" and \"keywords\".\n `\n}\n\n/**\n * Reads the model's response.\n *\n * Deliberately strict about a blank `altText`: the field is required on the\n * collection, so an empty string would satisfy that requirement while telling a\n * screen reader nothing — and nobody looks at an alt text again once it is set.\n */\nfunction parseResults(content: unknown, locales: string[]): null | Record<string, AltTextResult> {\n const parsed = schemaForLocales(locales).safeParse(content)\n\n if (!parsed.success) {\n return null\n }\n\n const results: Record<string, AltTextResult> = {}\n\n for (const locale of locales) {\n const entry = parsed.data[locale]\n\n if (entry.altText.trim().length === 0) {\n return null\n }\n\n results[locale] = { altText: entry.altText.trim(), keywords: entry.keywords }\n }\n\n return results\n}\n\n/**\n * One request, one or more locales.\n *\n * All locales go into a single call rather than one call each: the image is\n * uploaded and analyzed once — the expensive part — and every language ends up\n * describing the same reading of it.\n */\nasync function generate({\n apiKey,\n baseUrl,\n filename,\n imageThumbnailUrl,\n locales,\n model,\n timeoutMs,\n}: {\n apiKey: string\n baseUrl: string\n filename?: string\n imageThumbnailUrl: string\n locales: string[]\n model: string\n timeoutMs: number\n}): Promise<\n { error: string; success: false } | { results: Record<string, AltTextResult>; success: true }\n> {\n if (!apiKey) {\n return { error: 'No Mistral API key configured', success: false }\n }\n\n if (locales.length === 0) {\n return { error: 'No locale requested', success: false }\n }\n\n const signal = AbortSignal.timeout(timeoutMs)\n const image = await fetchImageAsDataUri(imageThumbnailUrl, signal)\n\n if ('error' in image) {\n return { error: image.error, success: false }\n }\n\n try {\n const response = await fetch(`${baseUrl}/chat/completions`, {\n body: JSON.stringify({\n max_tokens: 150 * locales.length,\n messages: [\n { content: buildPrompt(locales), 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: {\n name: 'data',\n schema: z.toJSONSchema(schemaForLocales(locales), { target: 'draft-7' }),\n strict: true,\n },\n },\n }),\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },\n method: 'POST',\n signal,\n })\n\n if (!response.ok) {\n const body = await response.text().catch(() => '')\n\n return {\n error: `Mistral responded with status ${response.status}${body ? `: ${body}` : ''}`,\n success: false,\n }\n }\n\n const completion = (await response.json()) as {\n choices?: { message?: { content?: unknown } }[]\n }\n const content = completion.choices?.[0]?.message?.content\n\n if (typeof content !== 'string') {\n return { error: 'No result from Mistral', success: false }\n }\n\n const results = parseResults(JSON.parse(content), locales)\n\n if (!results) {\n return {\n error: `Mistral did not return a usable alt text for every requested locale (${locales.join(', ')})`,\n success: false,\n }\n }\n\n return { results, success: true }\n } catch (error) {\n console.error('Error generating alt text:', error)\n\n return { error: error instanceof Error ? error.message : 'Unknown error', success: false }\n }\n}\n\n/**\n * Creates a Mistral-based resolver for alt text generation.\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 = (config: MistralResolverConfig): AltTextResolver => {\n const {\n apiKey,\n baseUrl = 'https://api.mistral.ai/v1',\n model = 'mistral-medium-latest',\n timeoutMs = 30_000,\n } = config\n\n return {\n key: 'mistral',\n resolve: async ({\n filename,\n imageThumbnailUrl,\n locale,\n }: AltTextResolverArgs): Promise<AltTextResolverResponse> => {\n const result = await generate({\n apiKey,\n baseUrl,\n filename,\n imageThumbnailUrl,\n locales: [locale],\n model,\n timeoutMs,\n })\n\n if (!result.success) {\n return { error: result.error, success: false }\n }\n\n return { result: result.results[locale], success: true }\n },\n resolveBulk: async ({\n filename,\n imageThumbnailUrl,\n locales,\n }: AltTextBulkResolverArgs): Promise<AltTextBulkResolverResponse> => {\n const result = await generate({\n apiKey,\n baseUrl,\n filename,\n imageThumbnailUrl,\n locales,\n model,\n timeoutMs,\n })\n\n if (!result.success) {\n return { error: result.error, success: false }\n }\n\n return { results: result.results, success: true }\n },\n // https://docs.mistral.ai/capabilities/vision/\n supportedMimeTypes: SUPPORTED_MIME_TYPES,\n }\n}\n"],"names":["z","SUPPORTED_MIME_TYPES","MAX_IMAGE_BYTES","altTextSchema","object","altText","string","describe","keywords","array","schemaForLocales","locales","Object","fromEntries","map","locale","fetchImageAsDataUri","url","signal","response","fetch","error","Error","message","ok","status","contentType","headers","get","split","trim","toLowerCase","includes","join","bytes","Buffer","from","arrayBuffer","byteLength","Math","round","dataUri","toString","buildPrompt","languages","parseResults","content","parsed","safeParse","success","results","entry","data","length","generate","apiKey","baseUrl","filename","imageThumbnailUrl","model","timeoutMs","AbortSignal","timeout","image","body","JSON","stringify","max_tokens","messages","role","type","image_url","text","response_format","json_schema","name","schema","toJSONSchema","target","strict","Authorization","method","catch","completion","json","choices","parse","console","mistralResolver","config","key","resolve","result","resolveBulk","supportedMimeTypes"],"mappings":"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"}
|
|
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,13 +1,21 @@
|
|
|
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'
|
|
@@ -23,10 +31,21 @@ export type OpenAIResolverConfig = {
|
|
|
23
31
|
* @default ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
|
24
32
|
*/
|
|
25
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;
|
|
26
40
|
};
|
|
27
41
|
/**
|
|
28
42
|
* Creates an OpenAI-based resolver for alt text generation.
|
|
29
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
|
+
*
|
|
30
49
|
* @example
|
|
31
50
|
* ```typescript
|
|
32
51
|
* import { openAIResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
@@ -45,4 +64,4 @@ export type OpenAIResolverConfig = {
|
|
|
45
64
|
* })
|
|
46
65
|
* ```
|
|
47
66
|
*/
|
|
48
|
-
export declare const openAIResolver: (
|
|
67
|
+
export declare const openAIResolver: ({ apiKey, baseUrl, instructions, model, supportedMimeTypes, timeoutMs, }: OpenAIResolverConfig) => AltTextResolver;
|