@jhb.software/payload-alt-text-plugin 0.6.0 → 0.7.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/dist/endpoints/bulkGenerateAltTexts.js +4 -16
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +4 -18
- package/dist/endpoints/generateAltText.js.map +1 -1
- package/dist/endpoints/schemas.d.ts +19 -0
- package/dist/endpoints/schemas.js +26 -0
- package/dist/endpoints/schemas.js.map +1 -0
- package/dist/hooks/revalidateAltTextHealth.js +4 -4
- package/dist/hooks/revalidateAltTextHealth.js.map +1 -1
- package/dist/resolvers/openAI.d.ts +14 -0
- package/dist/resolvers/openAI.js +13 -7
- package/dist/resolvers/openAI.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import pMap from 'p-map';
|
|
2
|
-
import {
|
|
2
|
+
import { ZodError } from 'zod';
|
|
3
3
|
import { localesFromConfig } from '../utilities/localesFromConfig.js';
|
|
4
4
|
import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
5
|
+
import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js';
|
|
5
6
|
/**
|
|
6
7
|
* Generates and updates alt text for multiple images in all locales.
|
|
7
8
|
*/ export const bulkGenerateAltTextsEndpoint = (access)=>async (req)=>{
|
|
@@ -16,14 +17,7 @@ import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
|
16
17
|
});
|
|
17
18
|
}
|
|
18
19
|
const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null;
|
|
19
|
-
const
|
|
20
|
-
collection: z.string(),
|
|
21
|
-
ids: z.array(z.union([
|
|
22
|
-
z.string(),
|
|
23
|
-
z.number()
|
|
24
|
-
]))
|
|
25
|
-
});
|
|
26
|
-
const { collection, ids } = schema.parse(data);
|
|
20
|
+
const { collection, ids } = bulkGenerateAltTextsRequestSchema.parse(data);
|
|
27
21
|
let updatedDocs = 0;
|
|
28
22
|
const erroredDocs = [];
|
|
29
23
|
// Get plugin config from payload config
|
|
@@ -84,13 +78,7 @@ import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
|
84
78
|
});
|
|
85
79
|
} catch (error) {
|
|
86
80
|
if (error instanceof ZodError) {
|
|
87
|
-
return Response.json({
|
|
88
|
-
details: error.issues.map((e)=>({
|
|
89
|
-
message: e.message,
|
|
90
|
-
path: e.path.join('.')
|
|
91
|
-
})),
|
|
92
|
-
error: 'Validation failed'
|
|
93
|
-
}, {
|
|
81
|
+
return Response.json(formatZodError(error), {
|
|
94
82
|
status: 400
|
|
95
83
|
});
|
|
96
84
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { z, ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\n\n/**\n * Generates and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const schema = z.object({\n collection: z.string(),\n ids: z.array(z.union([z.string(), z.number()])),\n })\n\n const { collection, ids } = schema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: (number | string)[] = []\n\n // Get plugin config from payload config\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // determine target locales based on config\n const locales = localesFromConfig(req.payload.config)\n const targetLocales = locales ?? [pluginConfig.locale!]\n if (!targetLocales) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n ids,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n updatedDocs++\n console.log(\n `${updatedDocs}/${ids.length} updated (${Math.round((updatedDocs / ids.length) * 100)}%)`,\n )\n } catch (error) {\n console.error(`Error generating alt text for ${id}:`, error)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n console.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n totalDocs: ids.length,\n updatedDocs,\n })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(\n {\n details: error.issues.map((e) => ({\n message: e.message,\n path: e.path.join('.'),\n })),\n error: 'Validation failed',\n },\n { status: 400 },\n )\n }\n console.error('Error in bulk generation:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: number | string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}) {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined\n\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n throw new Error(\n `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n )\n }\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n throw new Error(\n `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n })\n }\n }\n}\n"],"names":["pMap","z","ZodError","localesFromConfig","matchesMimeType","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","schema","object","collection","string","ids","array","union","number","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","resolver","concurrency","maxBulkGenerateConcurrency","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","length","Math","round","push","join","totalDocs","details","issues","map","e","message","path","Error","imageDoc","findByID","depth","mimeType","undefined","collectionConfig","collections","find","entry","slug","mimeTypes","supportedMimeTypes","includes","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","success","localeResult","results","update","alt","altText","keywords"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\nimport { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'\n\n/**\n * Generates and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { collection, ids } = bulkGenerateAltTextsRequestSchema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: (number | string)[] = []\n\n // Get plugin config from payload config\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // determine target locales based on config\n const locales = localesFromConfig(req.payload.config)\n const targetLocales = locales ?? [pluginConfig.locale!]\n if (!targetLocales) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n ids,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n updatedDocs++\n console.log(\n `${updatedDocs}/${ids.length} updated (${Math.round((updatedDocs / ids.length) * 100)}%)`,\n )\n } catch (error) {\n console.error(`Error generating alt text for ${id}:`, error)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n console.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n totalDocs: ids.length,\n updatedDocs,\n })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n console.error('Error in bulk generation:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: number | string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}) {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined\n\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n throw new Error(\n `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n )\n }\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n throw new Error(\n `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n })\n }\n }\n}\n"],"names":["pMap","ZodError","localesFromConfig","matchesMimeType","bulkGenerateAltTextsRequestSchema","formatZodError","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","collection","ids","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","resolver","concurrency","maxBulkGenerateConcurrency","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","length","Math","round","push","join","totalDocs","Error","message","imageDoc","findByID","depth","mimeType","undefined","collectionConfig","collections","find","entry","slug","mimeTypes","supportedMimeTypes","includes","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","success","localeResult","results","update","alt","altText","keywords"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ZodError } from 'zod';
|
|
2
2
|
import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
3
|
+
import { formatZodError, generateAltTextRequestSchema } from './schemas.js';
|
|
3
4
|
/**
|
|
4
5
|
* Generates alt text for a single image using the configured resolver.
|
|
5
6
|
*
|
|
@@ -20,16 +21,7 @@ import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
23
|
const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null;
|
|
23
|
-
const
|
|
24
|
-
id: z.union([
|
|
25
|
-
z.string(),
|
|
26
|
-
z.number()
|
|
27
|
-
]),
|
|
28
|
-
collection: z.string(),
|
|
29
|
-
locale: z.string().nullable(),
|
|
30
|
-
update: z.boolean().optional().default(false)
|
|
31
|
-
});
|
|
32
|
-
const { id, collection, locale, update } = requestSchema.parse(data);
|
|
24
|
+
const { id, collection, locale, update } = generateAltTextRequestSchema.parse(data);
|
|
33
25
|
const imageDoc = await req.payload.findByID({
|
|
34
26
|
id,
|
|
35
27
|
collection,
|
|
@@ -121,13 +113,7 @@ import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
|
121
113
|
});
|
|
122
114
|
} catch (error) {
|
|
123
115
|
if (error instanceof ZodError) {
|
|
124
|
-
return Response.json({
|
|
125
|
-
details: error.issues.map((e)=>({
|
|
126
|
-
message: e.message,
|
|
127
|
-
path: e.path.join('.')
|
|
128
|
-
})),
|
|
129
|
-
error: 'Validation failed'
|
|
130
|
-
}, {
|
|
116
|
+
return Response.json(formatZodError(error), {
|
|
131
117
|
status: 400
|
|
132
118
|
});
|
|
133
119
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\nimport { formatZodError, generateAltTextRequestSchema } from './schemas.js'\n\n/**\n * Generates alt text for a single image using the configured resolver.\n *\n * By default, returns the result without updating the document (preview mode).\n * Pass `update: true` in the request body to also persist the generated alt text\n * and keywords to the document — useful for programmatic/agent workflows.\n *\n * The response always includes the `id` and `collection` for easy correlation.\n */\nexport const generateAltTextEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { id, collection, locale, update } = generateAltTextRequestSchema.parse(data)\n\n const imageDoc = await req.payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n return Response.json({ error: 'Image not found' }, { status: 404 })\n }\n\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n if (!pluginConfig.getImageThumbnail) {\n return Response.json(\n { error: 'getImageThumbnail function not configured' },\n { status: 500 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string'\n ? imageDoc.mimeType\n : undefined\n\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n return Response.json(\n {\n error: `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n return Response.json(\n {\n error: `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locale\n const targetLocale = locale ?? pluginConfig.locale\n if (!targetLocale) {\n return Response.json(\n {\n error:\n 'Could not determine target locale for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolve({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locale: targetLocale,\n req,\n })\n\n if (!result.success) {\n return Response.json(\n { error: result.error || 'Failed to generate alt text' },\n { status: 500 },\n )\n }\n\n if (update) {\n await req.payload.update({\n id,\n collection,\n data: {\n alt: result.result.altText,\n keywords: result.result.keywords,\n },\n locale: targetLocale,\n })\n }\n\n return Response.json({ id, collection, ...result.result })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n console.error('Error generating alt text:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n"],"names":["ZodError","matchesMimeType","formatZodError","generateAltTextRequestSchema","generateAltTextEndpoint","access","req","Response","json","error","status","data","id","collection","locale","update","parse","imageDoc","payload","findByID","depth","pluginConfig","config","custom","altTextPluginConfig","getImageThumbnail","resolver","mimeType","undefined","collectionConfig","collections","find","entry","slug","mimeTypes","join","supportedMimeTypes","includes","targetLocale","imageThumbnailUrl","result","resolve","filename","success","alt","altText","keywords","console","Error","message"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ZodError } from 'zod';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const generateAltTextRequestSchema: z.ZodObject<{
|
|
4
|
+
id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
5
|
+
collection: z.ZodString;
|
|
6
|
+
locale: z.ZodNullable<z.ZodString>;
|
|
7
|
+
update: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export declare const bulkGenerateAltTextsRequestSchema: z.ZodObject<{
|
|
10
|
+
collection: z.ZodString;
|
|
11
|
+
ids: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
export declare const formatZodError: (error: ZodError) => {
|
|
14
|
+
details: {
|
|
15
|
+
message: string;
|
|
16
|
+
path: string;
|
|
17
|
+
}[];
|
|
18
|
+
error: string;
|
|
19
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const generateAltTextRequestSchema = z.object({
|
|
3
|
+
id: z.union([
|
|
4
|
+
z.string(),
|
|
5
|
+
z.number()
|
|
6
|
+
]),
|
|
7
|
+
collection: z.string(),
|
|
8
|
+
locale: z.string().nullable(),
|
|
9
|
+
update: z.boolean().optional().default(false)
|
|
10
|
+
});
|
|
11
|
+
export const bulkGenerateAltTextsRequestSchema = z.object({
|
|
12
|
+
collection: z.string(),
|
|
13
|
+
ids: z.array(z.union([
|
|
14
|
+
z.string(),
|
|
15
|
+
z.number()
|
|
16
|
+
]))
|
|
17
|
+
});
|
|
18
|
+
export const formatZodError = (error)=>({
|
|
19
|
+
details: error.issues.map((e)=>({
|
|
20
|
+
message: e.message,
|
|
21
|
+
path: e.path.join('.')
|
|
22
|
+
})),
|
|
23
|
+
error: 'Validation failed'
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/schemas.ts"],"sourcesContent":["import type { ZodError } from 'zod'\n\nimport { z } from 'zod'\n\nexport const generateAltTextRequestSchema = z.object({\n id: z.union([z.string(), z.number()]),\n collection: z.string(),\n locale: z.string().nullable(),\n update: z.boolean().optional().default(false),\n})\n\nexport const bulkGenerateAltTextsRequestSchema = z.object({\n collection: z.string(),\n ids: z.array(z.union([z.string(), z.number()])),\n})\n\nexport const formatZodError = (error: ZodError) => ({\n details: error.issues.map((e) => ({\n message: e.message,\n path: e.path.join('.'),\n })),\n error: 'Validation failed',\n})\n"],"names":["z","generateAltTextRequestSchema","object","id","union","string","number","collection","locale","nullable","update","boolean","optional","default","bulkGenerateAltTextsRequestSchema","ids","array","formatZodError","error","details","issues","map","e","message","path","join"],"mappings":"AAEA,SAASA,CAAC,QAAQ,MAAK;AAEvB,OAAO,MAAMC,+BAA+BD,EAAEE,MAAM,CAAC;IACnDC,IAAIH,EAAEI,KAAK,CAAC;QAACJ,EAAEK,MAAM;QAAIL,EAAEM,MAAM;KAAG;IACpCC,YAAYP,EAAEK,MAAM;IACpBG,QAAQR,EAAEK,MAAM,GAAGI,QAAQ;IAC3BC,QAAQV,EAAEW,OAAO,GAAGC,QAAQ,GAAGC,OAAO,CAAC;AACzC,GAAE;AAEF,OAAO,MAAMC,oCAAoCd,EAAEE,MAAM,CAAC;IACxDK,YAAYP,EAAEK,MAAM;IACpBU,KAAKf,EAAEgB,KAAK,CAAChB,EAAEI,KAAK,CAAC;QAACJ,EAAEK,MAAM;QAAIL,EAAEM,MAAM;KAAG;AAC/C,GAAE;AAEF,OAAO,MAAMW,iBAAiB,CAACC,QAAqB,CAAA;QAClDC,SAASD,MAAME,MAAM,CAACC,GAAG,CAAC,CAACC,IAAO,CAAA;gBAChCC,SAASD,EAAEC,OAAO;gBAClBC,MAAMF,EAAEE,IAAI,CAACC,IAAI,CAAC;YACpB,CAAA;QACAP,OAAO;IACT,CAAA,EAAE"}
|
|
@@ -3,11 +3,11 @@ import { ALT_TEXT_HEALTH_PLUGIN_SLUG, getAltTextHealthCollectionTag } from '../u
|
|
|
3
3
|
function safeRevalidateTag(req, tag) {
|
|
4
4
|
try {
|
|
5
5
|
// Support both Next 15 and Next 16. Next 15 types `revalidateTag(tag)` as 1-arg; Next 16
|
|
6
|
-
// added a required second `profile` arg
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// added a required second `profile` arg and logs a deprecation warning for 1-arg calls.
|
|
7
|
+
// Passing 'max' satisfies Next 16 and is ignored at runtime by Next 15. The cast lets the
|
|
8
|
+
// build succeed regardless of which Next types are resolved from the consuming project.
|
|
9
9
|
;
|
|
10
|
-
revalidateTag(tag);
|
|
10
|
+
revalidateTag(tag, 'max');
|
|
11
11
|
} catch (error) {
|
|
12
12
|
const message = error instanceof Error ? error.message : String(error);
|
|
13
13
|
if (message.includes('static generation store missing')) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/revalidateAltTextHealth.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'\n\nimport { revalidateTag } from 'next/cache.js'\n\nimport {\n ALT_TEXT_HEALTH_PLUGIN_SLUG,\n getAltTextHealthCollectionTag,\n} from '../utilities/altTextHealth.js'\n\nfunction safeRevalidateTag(req: PayloadRequest, tag: string): void {\n try {\n // Support both Next 15 and Next 16. Next 15 types `revalidateTag(tag)` as 1-arg; Next 16\n // added a required second `profile` arg
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/revalidateAltTextHealth.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'\n\nimport { revalidateTag } from 'next/cache.js'\n\nimport {\n ALT_TEXT_HEALTH_PLUGIN_SLUG,\n getAltTextHealthCollectionTag,\n} from '../utilities/altTextHealth.js'\n\nfunction safeRevalidateTag(req: PayloadRequest, tag: string): void {\n try {\n // Support both Next 15 and Next 16. Next 15 types `revalidateTag(tag)` as 1-arg; Next 16\n // added a required second `profile` arg and logs a deprecation warning for 1-arg calls.\n // Passing 'max' satisfies Next 16 and is ignored at runtime by Next 15. The cast lets the\n // build succeed regardless of which Next types are resolved from the consuming project.\n ;(revalidateTag as (tag: string, profile?: string) => void)(tag, 'max')\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n if (message.includes('static generation store missing')) {\n req.payload.logger.warn({\n msg: 'Skipping alt text health cache revalidation outside a Next.js request context.',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n tag,\n })\n return\n }\n\n throw error\n }\n}\n\nexport const createRevalidateAltTextHealthAfterChangeHook =\n (collectionSlug: string): CollectionAfterChangeHook =>\n ({ doc, req }) => {\n if (!req.context?.disableRevalidate) {\n safeRevalidateTag(req, getAltTextHealthCollectionTag(collectionSlug))\n }\n\n return doc\n }\n\nexport const createRevalidateAltTextHealthAfterDeleteHook =\n (collectionSlug: string): CollectionAfterDeleteHook =>\n ({ doc, req }) => {\n if (!req.context?.disableRevalidate) {\n safeRevalidateTag(req, getAltTextHealthCollectionTag(collectionSlug))\n }\n\n return doc\n }\n"],"names":["revalidateTag","ALT_TEXT_HEALTH_PLUGIN_SLUG","getAltTextHealthCollectionTag","safeRevalidateTag","req","tag","error","message","Error","String","includes","payload","logger","warn","msg","plugin","createRevalidateAltTextHealthAfterChangeHook","collectionSlug","doc","context","disableRevalidate","createRevalidateAltTextHealthAfterDeleteHook"],"mappings":"AAEA,SAASA,aAAa,QAAQ,gBAAe;AAE7C,SACEC,2BAA2B,EAC3BC,6BAA6B,QACxB,gCAA+B;AAEtC,SAASC,kBAAkBC,GAAmB,EAAEC,GAAW;IACzD,IAAI;QACF,yFAAyF;QACzF,wFAAwF;QACxF,0FAA0F;QAC1F,wFAAwF;;QACtFL,cAA0DK,KAAK;IACnE,EAAE,OAAOC,OAAO;QACd,MAAMC,UAAUD,iBAAiBE,QAAQF,MAAMC,OAAO,GAAGE,OAAOH;QAEhE,IAAIC,QAAQG,QAAQ,CAAC,oCAAoC;YACvDN,IAAIO,OAAO,CAACC,MAAM,CAACC,IAAI,CAAC;gBACtBC,KAAK;gBACLC,QAAQd;gBACRI;YACF;YACA;QACF;QAEA,MAAMC;IACR;AACF;AAEA,OAAO,MAAMU,+CACX,CAACC,iBACD,CAAC,EAAEC,GAAG,EAAEd,GAAG,EAAE;QACX,IAAI,CAACA,IAAIe,OAAO,EAAEC,mBAAmB;YACnCjB,kBAAkBC,KAAKF,8BAA8Be;QACvD;QAEA,OAAOC;IACT,EAAC;AAEH,OAAO,MAAMG,+CACX,CAACJ,iBACD,CAAC,EAAEC,GAAG,EAAEd,GAAG,EAAE;QACX,IAAI,CAACA,IAAIe,OAAO,EAAEC,mBAAmB;YACnCjB,kBAAkBC,KAAKF,8BAA8Be;QACvD;QAEA,OAAOC;IACT,EAAC"}
|
|
@@ -2,6 +2,12 @@ import type { AltTextResolver } from './types.js';
|
|
|
2
2
|
export type OpenAIResolverConfig = {
|
|
3
3
|
/** OpenAI API key for authentication */
|
|
4
4
|
apiKey: string;
|
|
5
|
+
/**
|
|
6
|
+
* Base URL for the OpenAI-compatible API.
|
|
7
|
+
* Use this to point at alternative providers (e.g. Azure, Nebius, local inference).
|
|
8
|
+
* @default undefined — the OpenAI SDK defaults to 'https://api.openai.com/v1'
|
|
9
|
+
*/
|
|
10
|
+
baseUrl?: string;
|
|
5
11
|
/**
|
|
6
12
|
* The OpenAI LLM model to use for alt text generation.
|
|
7
13
|
* @default 'gpt-4.1-nano'
|
|
@@ -15,10 +21,18 @@ export type OpenAIResolverConfig = {
|
|
|
15
21
|
* ```typescript
|
|
16
22
|
* import { openAIResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
17
23
|
*
|
|
24
|
+
* // OpenAI
|
|
18
25
|
* openAIResolver({
|
|
19
26
|
* apiKey: process.env.OPENAI_API_KEY,
|
|
20
27
|
* model: 'gpt-4.1-mini', // optional, defaults to 'gpt-4.1-nano'
|
|
21
28
|
* })
|
|
29
|
+
*
|
|
30
|
+
* // OpenAI-compatible provider (e.g. Nebius)
|
|
31
|
+
* openAIResolver({
|
|
32
|
+
* apiKey: process.env.NEBIUS_API_KEY,
|
|
33
|
+
* baseUrl: 'https://api.tokenfactory.us-central1.nebius.com/v1',
|
|
34
|
+
* model: 'Qwen/Qwen2.5-VL-72B-Instruct',
|
|
35
|
+
* })
|
|
22
36
|
* ```
|
|
23
37
|
*/
|
|
24
38
|
export declare const openAIResolver: (config: OpenAIResolverConfig) => AltTextResolver;
|
package/dist/resolvers/openAI.js
CHANGED
|
@@ -27,20 +27,29 @@ import { z } from 'zod';
|
|
|
27
27
|
* ```typescript
|
|
28
28
|
* import { openAIResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
29
29
|
*
|
|
30
|
+
* // OpenAI
|
|
30
31
|
* openAIResolver({
|
|
31
32
|
* apiKey: process.env.OPENAI_API_KEY,
|
|
32
33
|
* model: 'gpt-4.1-mini', // optional, defaults to 'gpt-4.1-nano'
|
|
33
34
|
* })
|
|
35
|
+
*
|
|
36
|
+
* // OpenAI-compatible provider (e.g. Nebius)
|
|
37
|
+
* openAIResolver({
|
|
38
|
+
* apiKey: process.env.NEBIUS_API_KEY,
|
|
39
|
+
* baseUrl: 'https://api.tokenfactory.us-central1.nebius.com/v1',
|
|
40
|
+
* model: 'Qwen/Qwen2.5-VL-72B-Instruct',
|
|
41
|
+
* })
|
|
34
42
|
* ```
|
|
35
43
|
*/ export const openAIResolver = (config)=>{
|
|
36
|
-
const { apiKey, model = 'gpt-4.1-nano' } = config;
|
|
44
|
+
const { apiKey, baseUrl, model = 'gpt-4.1-nano' } = config;
|
|
45
|
+
const openai = new OpenAI({
|
|
46
|
+
apiKey,
|
|
47
|
+
baseURL: baseUrl
|
|
48
|
+
});
|
|
37
49
|
return {
|
|
38
50
|
key: 'openai',
|
|
39
51
|
resolve: async ({ filename, imageThumbnailUrl, locale })=>{
|
|
40
52
|
try {
|
|
41
|
-
const openai = new OpenAI({
|
|
42
|
-
apiKey
|
|
43
|
-
});
|
|
44
53
|
const modelResponseSchema = z.object({
|
|
45
54
|
altText: z.string().describe('A concise, descriptive alt text for the image'),
|
|
46
55
|
keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
|
|
@@ -104,9 +113,6 @@ import { z } from 'zod';
|
|
|
104
113
|
},
|
|
105
114
|
resolveBulk: async ({ filename, imageThumbnailUrl, locales })=>{
|
|
106
115
|
try {
|
|
107
|
-
const openai = new OpenAI({
|
|
108
|
-
apiKey
|
|
109
|
-
});
|
|
110
116
|
const modelResponseSchema = z.object(Object.fromEntries(locales.map((locale)=>[
|
|
111
117
|
locale,
|
|
112
118
|
z.object({
|
|
@@ -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 * 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 * openAIResolver({\n * apiKey: process.env.OPENAI_API_KEY,\n * model: 'gpt-4.1-mini', // optional, defaults to 'gpt-4.1-nano'\n * })\n * ```\n */\nexport const openAIResolver = (config: OpenAIResolverConfig): AltTextResolver => {\n const { apiKey, model = 'gpt-4.1-nano' } = config\n\n return {\n key: 'openai',\n resolve: async ({\n filename,\n imageThumbnailUrl,\n locale,\n }: AltTextResolverArgs): Promise<AltTextResolverResponse> => {\n try {\n const openai = new OpenAI({ apiKey })\n\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 openai.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 openai = new OpenAI({ apiKey })\n\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 openai.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","model","key","resolve","filename","imageThumbnailUrl","locale","openai","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":"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"}
|
|
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 const 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 openai.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 openai.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","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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhb.software/payload-alt-text-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A Payload CMS plugin that adds AI-powered alt text generation for images.",
|
|
5
5
|
"bugs": "https://github.com/jhb-software/payload-plugins/issues",
|
|
6
6
|
"repository": "https://github.com/jhb-software/payload-plugins",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@types/react": "19.2.14",
|
|
38
38
|
"@types/react-dom": "19.2.3",
|
|
39
39
|
"copyfiles": "2.4.1",
|
|
40
|
-
"eslint": "^9.
|
|
40
|
+
"eslint": "^9.39.4",
|
|
41
41
|
"prettier": "^3.8.3",
|
|
42
42
|
"rimraf": "6.1.3",
|
|
43
43
|
"typescript": "^6.0.3"
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
"engines": {
|
|
66
|
-
"node": "
|
|
66
|
+
"node": ">=22.12.0"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|