@jhb.software/payload-alt-text-plugin 0.1.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/LICENSE.md +21 -0
- package/README.md +54 -0
- package/dist/components/AltTextField.d.ts +2 -0
- package/dist/components/AltTextField.js +40 -0
- package/dist/components/AltTextField.js.map +1 -0
- package/dist/components/BulkGenerateAltTextsButton.d.ts +1 -0
- package/dist/components/BulkGenerateAltTextsButton.js +69 -0
- package/dist/components/BulkGenerateAltTextsButton.js.map +1 -0
- package/dist/components/GenerateAltTextButton.d.ts +1 -0
- package/dist/components/GenerateAltTextButton.js +109 -0
- package/dist/components/GenerateAltTextButton.js.map +1 -0
- package/dist/components/icons/Lightning.d.ts +1 -0
- package/dist/components/icons/Lightning.js +19 -0
- package/dist/components/icons/Lightning.js.map +1 -0
- package/dist/components/icons/Spinner.d.ts +1 -0
- package/dist/components/icons/Spinner.js +42 -0
- package/dist/components/icons/Spinner.js.map +1 -0
- package/dist/endpoints/bulkGenerateAltTexts.d.ts +5 -0
- package/dist/endpoints/bulkGenerateAltTexts.js +150 -0
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -0
- package/dist/endpoints/generateAltText.d.ts +6 -0
- package/dist/endpoints/generateAltText.js +128 -0
- package/dist/endpoints/generateAltText.js.map +1 -0
- package/dist/exports/client.d.ts +3 -0
- package/dist/exports/client.js +5 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/fields/altTextField.d.ts +4 -0
- package/dist/fields/altTextField.js +27 -0
- package/dist/fields/altTextField.js.map +1 -0
- package/dist/fields/keywordsField.d.ts +4 -0
- package/dist/fields/keywordsField.js +17 -0
- package/dist/fields/keywordsField.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +90 -0
- package/dist/plugin.js.map +1 -0
- package/dist/types/AltTextPluginConfig.d.ts +48 -0
- package/dist/types/AltTextPluginConfig.js +3 -0
- package/dist/types/AltTextPluginConfig.js.map +1 -0
- package/dist/utilities/getGenerationCost.d.ts +12 -0
- package/dist/utilities/getGenerationCost.js +30 -0
- package/dist/utilities/getGenerationCost.js.map +1 -0
- package/dist/utilities/zodResponseFormat.d.ts +11 -0
- package/dist/utilities/zodResponseFormat.js +23 -0
- package/dist/utilities/zodResponseFormat.js.map +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getGenerationCost } from '../utilities/getGenerationCost.js';
|
|
4
|
+
import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generates alt text for a single image using OpenAI Vision API.
|
|
7
|
+
* Returns result without updating the document.
|
|
8
|
+
*/ export const generateAltTextEndpoint = async (req)=>{
|
|
9
|
+
try {
|
|
10
|
+
if (!req.user) {
|
|
11
|
+
return Response.json({
|
|
12
|
+
error: 'Unauthorized'
|
|
13
|
+
}, {
|
|
14
|
+
status: 401
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null;
|
|
18
|
+
const requestSchema = z.object({
|
|
19
|
+
collection: z.string(),
|
|
20
|
+
id: z.string(),
|
|
21
|
+
locale: z.string()
|
|
22
|
+
});
|
|
23
|
+
const { collection, id, locale } = requestSchema.parse(data);
|
|
24
|
+
const imageDoc = await req.payload.findByID({
|
|
25
|
+
collection,
|
|
26
|
+
id,
|
|
27
|
+
depth: 0
|
|
28
|
+
});
|
|
29
|
+
if (!imageDoc) {
|
|
30
|
+
return Response.json({
|
|
31
|
+
error: 'Image not found'
|
|
32
|
+
}, {
|
|
33
|
+
status: 404
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const pluginConfig = req.payload.config.custom?.altTextPluginConfig;
|
|
37
|
+
if (!pluginConfig?.getImageThumbnail) {
|
|
38
|
+
return Response.json({
|
|
39
|
+
error: 'getImageThumbnail function not configured'
|
|
40
|
+
}, {
|
|
41
|
+
status: 500
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc);
|
|
45
|
+
if (!imageThumbnailUrl) {
|
|
46
|
+
return Response.json({
|
|
47
|
+
error: 'Image thumbnail URL not defined'
|
|
48
|
+
}, {
|
|
49
|
+
status: 500
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!imageThumbnailUrl.startsWith('https://') && !imageThumbnailUrl.includes('http://')) {
|
|
53
|
+
return Response.json({
|
|
54
|
+
error: 'Image thumbnail URL is not a valid URL. It must start with https:// or http://'
|
|
55
|
+
}, {
|
|
56
|
+
status: 500
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const openai = new OpenAI({
|
|
60
|
+
apiKey: pluginConfig.openAIApiKey
|
|
61
|
+
});
|
|
62
|
+
const modelResponseSchema = z.object({
|
|
63
|
+
altText: z.string().describe('A concise, descriptive alt text for the image'),
|
|
64
|
+
keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
|
|
65
|
+
});
|
|
66
|
+
const response = await openai.chat.completions.parse({
|
|
67
|
+
model: pluginConfig.model,
|
|
68
|
+
messages: [
|
|
69
|
+
{
|
|
70
|
+
role: 'system',
|
|
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
|
+
`
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
role: 'user',
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: 'image_url',
|
|
88
|
+
image_url: {
|
|
89
|
+
url: imageThumbnailUrl
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
...'filename' in imageDoc && imageDoc.filename ? [
|
|
93
|
+
{
|
|
94
|
+
type: 'text',
|
|
95
|
+
text: imageDoc.filename
|
|
96
|
+
}
|
|
97
|
+
] : []
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
// limit the response tokens and costs per request
|
|
102
|
+
max_completion_tokens: 150,
|
|
103
|
+
response_format: zodResponseFormat(modelResponseSchema, 'data')
|
|
104
|
+
});
|
|
105
|
+
console.log({
|
|
106
|
+
imageId: id,
|
|
107
|
+
...getGenerationCost(response, pluginConfig.model)
|
|
108
|
+
});
|
|
109
|
+
const result = response.choices[0]?.message?.parsed;
|
|
110
|
+
if (!result) {
|
|
111
|
+
return Response.json({
|
|
112
|
+
error: 'No result from OpenAI'
|
|
113
|
+
}, {
|
|
114
|
+
status: 500
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return Response.json(result);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('Error generating alt text:', error);
|
|
120
|
+
return Response.json({
|
|
121
|
+
error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
122
|
+
}, {
|
|
123
|
+
status: 500
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
//# sourceMappingURL=generateAltText.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { ChatCompletionContentPartText } from 'openai/resources/chat/completions.mjs'\nimport type { PayloadHandler, PayloadRequest } from 'payload'\nimport { z } from 'zod'\nimport { getGenerationCost } from '../utilities/getGenerationCost.js'\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\nimport { zodResponseFormat } from '../utilities/zodResponseFormat.js'\n\n/**\n * Generates alt text for a single image using OpenAI Vision API.\n * Returns result without updating the document.\n */\nexport const generateAltTextEndpoint: PayloadHandler = async (req: PayloadRequest) => {\n try {\n if (!req.user) {\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 requestSchema = z.object({\n collection: z.string(),\n id: z.string(),\n locale: z.string(),\n })\n\n const { collection, id, locale } = requestSchema.parse(data)\n\n const imageDoc = await req.payload.findByID({\n collection,\n id,\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?.getImageThumbnail) {\n return Response.json({ error: 'getImageThumbnail function not configured' }, { status: 500 })\n }\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n if (!imageThumbnailUrl) {\n return Response.json({ error: 'Image thumbnail URL not defined' }, { status: 500 })\n }\n\n if (!imageThumbnailUrl.startsWith('https://') && !imageThumbnailUrl.includes('http://')) {\n return Response.json(\n { error: 'Image thumbnail URL is not a valid URL. It must start with https:// or http://' },\n { status: 500 },\n )\n }\n\n const openai = new OpenAI({\n apiKey: pluginConfig.openAIApiKey,\n })\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 model: pluginConfig.model,\n messages: [\n {\n role: 'system',\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 },\n {\n role: 'user',\n content: [\n {\n type: 'image_url',\n image_url: { url: imageThumbnailUrl },\n },\n ...('filename' in imageDoc && imageDoc.filename\n ? [\n {\n type: 'text',\n text: imageDoc.filename,\n } satisfies ChatCompletionContentPartText,\n ]\n : []),\n ],\n },\n ],\n // limit the response tokens and costs per request\n max_completion_tokens: 150,\n response_format: zodResponseFormat(modelResponseSchema, 'data'),\n })\n\n console.log({ imageId: id, ...getGenerationCost(response, pluginConfig.model) })\n\n const result = response.choices[0]?.message?.parsed\n\n if (!result) {\n return Response.json({ error: 'No result from OpenAI' }, { status: 500 })\n }\n\n return Response.json(result)\n } catch (error) {\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":["OpenAI","z","getGenerationCost","zodResponseFormat","generateAltTextEndpoint","req","user","Response","json","error","status","data","requestSchema","object","collection","string","id","locale","parse","imageDoc","payload","findByID","depth","pluginConfig","config","custom","altTextPluginConfig","getImageThumbnail","imageThumbnailUrl","startsWith","includes","openai","apiKey","openAIApiKey","modelResponseSchema","altText","describe","keywords","array","response","chat","completions","model","messages","role","content","type","image_url","url","filename","text","max_completion_tokens","response_format","console","log","imageId","result","choices","message","parsed","Error"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AltTextField } from '../components/AltTextField.js';
|
|
2
|
+
export { GenerateAltTextButton } from '../components/GenerateAltTextButton.js';
|
|
3
|
+
export { BulkGenerateAltTextsButton } from '../components/BulkGenerateAltTextsButton.js';
|
|
4
|
+
|
|
5
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { AltTextField } from '../components/AltTextField.js'\nexport { GenerateAltTextButton } from '../components/GenerateAltTextButton.js'\nexport { BulkGenerateAltTextsButton } from '../components/BulkGenerateAltTextsButton.js'\n"],"names":["AltTextField","GenerateAltTextButton","BulkGenerateAltTextsButton"],"mappings":"AAAA,SAASA,YAAY,QAAQ,gCAA+B;AAC5D,SAASC,qBAAqB,QAAQ,yCAAwC;AAC9E,SAASC,0BAA0B,QAAQ,8CAA6C"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function altTextField({ localized }) {
|
|
2
|
+
return {
|
|
3
|
+
name: 'alt',
|
|
4
|
+
label: 'Alternate text',
|
|
5
|
+
type: 'textarea',
|
|
6
|
+
required: true,
|
|
7
|
+
localized: localized,
|
|
8
|
+
validate: (value, ctx)=>{
|
|
9
|
+
// if the document has an id, the alt text is required
|
|
10
|
+
if (ctx.id) {
|
|
11
|
+
if (!value || value.trim().length === 0) {
|
|
12
|
+
return 'The alternate text is required.';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// The alt text is not required when the document is created because the alt text generation
|
|
16
|
+
// can only be used once the document is created and the image uploaded
|
|
17
|
+
return true;
|
|
18
|
+
},
|
|
19
|
+
admin: {
|
|
20
|
+
components: {
|
|
21
|
+
Field: '@jhb.software/payload-alt-text-plugin/client#AltTextField'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=altTextField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/fields/altTextField.ts"],"sourcesContent":["import type { TextareaField } from 'payload'\n\nexport function altTextField({\n localized,\n}: {\n localized?: TextareaField['localized']\n}): TextareaField {\n return {\n name: 'alt',\n label: 'Alternate text',\n type: 'textarea',\n required: true,\n localized: localized,\n validate: (value, ctx) => {\n // if the document has an id, the alt text is required\n if (ctx.id) {\n if (!value || value.trim().length === 0) {\n return 'The alternate text is required.'\n }\n }\n\n // The alt text is not required when the document is created because the alt text generation\n // can only be used once the document is created and the image uploaded\n return true\n },\n admin: {\n components: {\n Field: '@jhb.software/payload-alt-text-plugin/client#AltTextField',\n },\n },\n }\n}\n"],"names":["altTextField","localized","name","label","type","required","validate","value","ctx","id","trim","length","admin","components","Field"],"mappings":"AAEA,OAAO,SAASA,aAAa,EAC3BC,SAAS,EAGV;IACC,OAAO;QACLC,MAAM;QACNC,OAAO;QACPC,MAAM;QACNC,UAAU;QACVJ,WAAWA;QACXK,UAAU,CAACC,OAAOC;YAChB,sDAAsD;YACtD,IAAIA,IAAIC,EAAE,EAAE;gBACV,IAAI,CAACF,SAASA,MAAMG,IAAI,GAAGC,MAAM,KAAK,GAAG;oBACvC,OAAO;gBACT;YACF;YAEA,4FAA4F;YAC5F,uEAAuE;YACvE,OAAO;QACT;QACAC,OAAO;YACLC,YAAY;gBACVC,OAAO;YACT;QACF;IACF;AACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function keywordsField({ localized }) {
|
|
2
|
+
return {
|
|
3
|
+
name: 'keywords',
|
|
4
|
+
label: 'Keywords',
|
|
5
|
+
type: 'text',
|
|
6
|
+
hasMany: true,
|
|
7
|
+
required: false,
|
|
8
|
+
localized: localized,
|
|
9
|
+
hidden: true,
|
|
10
|
+
admin: {
|
|
11
|
+
description: 'Keywords which describe the image. Used when searching for the image.',
|
|
12
|
+
readOnly: true
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=keywordsField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/fields/keywordsField.ts"],"sourcesContent":["import type { TextField } from 'payload'\n\nexport function keywordsField({ localized }: { localized?: TextField['localized'] }): TextField {\n return {\n name: 'keywords',\n label: 'Keywords',\n type: 'text',\n hasMany: true,\n required: false,\n localized: localized,\n hidden: true, // this field is only meant to be used for improving the search\n admin: {\n description: 'Keywords which describe the image. Used when searching for the image.',\n readOnly: true,\n },\n }\n}\n"],"names":["keywordsField","localized","name","label","type","hasMany","required","hidden","admin","description","readOnly"],"mappings":"AAEA,OAAO,SAASA,cAAc,EAAEC,SAAS,EAA0C;IACjF,OAAO;QACLC,MAAM;QACNC,OAAO;QACPC,MAAM;QACNC,SAAS;QACTC,UAAU;QACVL,WAAWA;QACXM,QAAQ;QACRC,OAAO;YACLC,aAAa;YACbC,UAAU;QACZ;IACF;AACF"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadAltTextPlugin } from './plugin.js'\nexport type { IncomingAltTextPluginConfig as AltTextPluginConfig } from './types/AltTextPluginConfig.js'\n"],"names":["payloadAltTextPlugin"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,cAAa"}
|
package/dist/plugin.d.ts
ADDED
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { altTextField } from './fields/altTextField.js';
|
|
2
|
+
import { keywordsField } from './fields/keywordsField.js';
|
|
3
|
+
import { generateAltTextEndpoint } from './endpoints/generateAltText.js';
|
|
4
|
+
import { bulkGenerateAltTextsEndpoint } from './endpoints/bulkGenerateAltTexts.js';
|
|
5
|
+
export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
6
|
+
const config = {
|
|
7
|
+
...incomingConfig
|
|
8
|
+
};
|
|
9
|
+
// If the plugin is disabled, return the config without modifying it
|
|
10
|
+
if (incomingPluginConfig.enabled === false) {
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
13
|
+
const locales = config.localization ? config.localization.locales.map((localeConfig)=>typeof localeConfig === 'string' ? localeConfig : localeConfig.code) : [];
|
|
14
|
+
if (locales.length === 0) {
|
|
15
|
+
throw new Error('The alt text plugin currently only supports localized setups. If you need to use this plugin in a non-localized setup, please open an issue at https://github.com/jhb-software/payload-plugins.');
|
|
16
|
+
}
|
|
17
|
+
const pluginConfig = {
|
|
18
|
+
enabled: incomingPluginConfig.enabled ?? true,
|
|
19
|
+
openAIApiKey: incomingPluginConfig.openAIApiKey,
|
|
20
|
+
collections: incomingPluginConfig.collections,
|
|
21
|
+
maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,
|
|
22
|
+
model: incomingPluginConfig.model ?? 'gpt-4.1-nano',
|
|
23
|
+
locales: locales,
|
|
24
|
+
getImageThumbnail: incomingPluginConfig.getImageThumbnail,
|
|
25
|
+
fieldsOverride: incomingPluginConfig.fieldsOverride
|
|
26
|
+
};
|
|
27
|
+
const defaultFields = [
|
|
28
|
+
altTextField({
|
|
29
|
+
localized: Boolean(config.localization)
|
|
30
|
+
}),
|
|
31
|
+
keywordsField({
|
|
32
|
+
localized: Boolean(config.localization)
|
|
33
|
+
})
|
|
34
|
+
];
|
|
35
|
+
const fields = incomingPluginConfig.fieldsOverride && typeof incomingPluginConfig.fieldsOverride === 'function' ? incomingPluginConfig.fieldsOverride({
|
|
36
|
+
defaultFields
|
|
37
|
+
}) : defaultFields;
|
|
38
|
+
// Ensure collections array exists
|
|
39
|
+
config.collections = config.collections || [];
|
|
40
|
+
// Map over collections and inject AI alt text fields into specified ones
|
|
41
|
+
config.collections = config.collections.map((collectionConfig)=>{
|
|
42
|
+
if (pluginConfig.collections.includes(collectionConfig.slug)) {
|
|
43
|
+
if (!collectionConfig.upload) {
|
|
44
|
+
console.warn(`AI Alt Text Plugin: Collection "${collectionConfig.slug}" is not an upload collection. Skipping field injection.`);
|
|
45
|
+
return collectionConfig;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...collectionConfig,
|
|
49
|
+
admin: {
|
|
50
|
+
...collectionConfig.admin,
|
|
51
|
+
components: {
|
|
52
|
+
...collectionConfig.admin?.components ?? {},
|
|
53
|
+
beforeListTable: [
|
|
54
|
+
...collectionConfig.admin?.components?.beforeListTable ?? [],
|
|
55
|
+
'@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton'
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
fields: [
|
|
60
|
+
...collectionConfig.fields ?? [],
|
|
61
|
+
...fields
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return collectionConfig;
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
...config,
|
|
69
|
+
custom: {
|
|
70
|
+
...config.custom,
|
|
71
|
+
// Make plugin config available in hooks/actions
|
|
72
|
+
altTextPluginConfig: pluginConfig
|
|
73
|
+
},
|
|
74
|
+
endpoints: [
|
|
75
|
+
...config.endpoints ?? [],
|
|
76
|
+
{
|
|
77
|
+
path: '/alt-text-plugin/generate-alt-text',
|
|
78
|
+
method: 'post',
|
|
79
|
+
handler: generateAltTextEndpoint
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
path: '/alt-text-plugin/bulk-generate-alt-texts',
|
|
83
|
+
method: 'post',
|
|
84
|
+
handler: bulkGenerateAltTextsEndpoint
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport type {\n AltTextPluginConfig,\n IncomingAltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\nimport { altTextField } from './fields/altTextField.js'\nimport { keywordsField } from './fields/keywordsField.js'\nimport { generateAltTextEndpoint } from './endpoints/generateAltText.js'\nimport { bulkGenerateAltTextsEndpoint } from './endpoints/bulkGenerateAltTexts.js'\n\nexport const payloadAltTextPlugin =\n (incomingPluginConfig: IncomingAltTextPluginConfig) =>\n (incomingConfig: Config): Config => {\n const config = { ...incomingConfig }\n\n // If the plugin is disabled, return the config without modifying it\n if (incomingPluginConfig.enabled === false) {\n return config\n }\n\n const locales = config.localization\n ? config.localization.locales.map((localeConfig) =>\n typeof localeConfig === 'string' ? localeConfig : localeConfig.code,\n )\n : []\n\n if (locales.length === 0) {\n throw new Error(\n 'The alt text plugin currently only supports localized setups. If you need to use this plugin in a non-localized setup, please open an issue at https://github.com/jhb-software/payload-plugins.',\n )\n }\n\n const pluginConfig: AltTextPluginConfig = {\n enabled: incomingPluginConfig.enabled ?? true,\n openAIApiKey: incomingPluginConfig.openAIApiKey,\n collections: incomingPluginConfig.collections,\n maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,\n model: incomingPluginConfig.model ?? 'gpt-4.1-nano',\n locales: locales,\n getImageThumbnail: incomingPluginConfig.getImageThumbnail,\n fieldsOverride: incomingPluginConfig.fieldsOverride,\n }\n\n const defaultFields = [\n altTextField({\n localized: Boolean(config.localization),\n }),\n keywordsField({\n localized: Boolean(config.localization),\n }),\n ]\n\n const fields =\n incomingPluginConfig.fieldsOverride &&\n typeof incomingPluginConfig.fieldsOverride === 'function'\n ? incomingPluginConfig.fieldsOverride({ defaultFields })\n : defaultFields\n\n // Ensure collections array exists\n config.collections = config.collections || []\n\n // Map over collections and inject AI alt text fields into specified ones\n config.collections = config.collections.map((collectionConfig) => {\n if (pluginConfig.collections.includes(collectionConfig.slug)) {\n if (!collectionConfig.upload) {\n console.warn(\n `AI Alt Text Plugin: Collection \"${collectionConfig.slug}\" is not an upload collection. Skipping field injection.`,\n )\n return collectionConfig\n }\n\n return {\n ...collectionConfig,\n admin: {\n ...collectionConfig.admin,\n components: {\n ...(collectionConfig.admin?.components ?? {}),\n beforeListTable: [\n ...(collectionConfig.admin?.components?.beforeListTable ?? []),\n '@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton',\n ],\n },\n },\n fields: [...(collectionConfig.fields ?? []), ...fields],\n }\n }\n\n return collectionConfig\n })\n\n return {\n ...config,\n custom: {\n ...config.custom,\n // Make plugin config available in hooks/actions\n altTextPluginConfig: pluginConfig,\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n path: '/alt-text-plugin/generate-alt-text',\n method: 'post',\n handler: generateAltTextEndpoint,\n },\n {\n path: '/alt-text-plugin/bulk-generate-alt-texts',\n method: 'post',\n handler: bulkGenerateAltTextsEndpoint,\n },\n ],\n }\n }\n"],"names":["altTextField","keywordsField","generateAltTextEndpoint","bulkGenerateAltTextsEndpoint","payloadAltTextPlugin","incomingPluginConfig","incomingConfig","config","enabled","locales","localization","map","localeConfig","code","length","Error","pluginConfig","openAIApiKey","collections","maxBulkGenerateConcurrency","model","getImageThumbnail","fieldsOverride","defaultFields","localized","Boolean","fields","collectionConfig","includes","slug","upload","console","warn","admin","components","beforeListTable","custom","altTextPluginConfig","endpoints","path","method","handler"],"mappings":"AAMA,SAASA,YAAY,QAAQ,2BAA0B;AACvD,SAASC,aAAa,QAAQ,4BAA2B;AACzD,SAASC,uBAAuB,QAAQ,iCAAgC;AACxE,SAASC,4BAA4B,QAAQ,sCAAqC;AAElF,OAAO,MAAMC,uBACX,CAACC,uBACD,CAACC;QACC,MAAMC,SAAS;YAAE,GAAGD,cAAc;QAAC;QAEnC,oEAAoE;QACpE,IAAID,qBAAqBG,OAAO,KAAK,OAAO;YAC1C,OAAOD;QACT;QAEA,MAAME,UAAUF,OAAOG,YAAY,GAC/BH,OAAOG,YAAY,CAACD,OAAO,CAACE,GAAG,CAAC,CAACC,eAC/B,OAAOA,iBAAiB,WAAWA,eAAeA,aAAaC,IAAI,IAErE,EAAE;QAEN,IAAIJ,QAAQK,MAAM,KAAK,GAAG;YACxB,MAAM,IAAIC,MACR;QAEJ;QAEA,MAAMC,eAAoC;YACxCR,SAASH,qBAAqBG,OAAO,IAAI;YACzCS,cAAcZ,qBAAqBY,YAAY;YAC/CC,aAAab,qBAAqBa,WAAW;YAC7CC,4BAA4Bd,qBAAqBc,0BAA0B,IAAI;YAC/EC,OAAOf,qBAAqBe,KAAK,IAAI;YACrCX,SAASA;YACTY,mBAAmBhB,qBAAqBgB,iBAAiB;YACzDC,gBAAgBjB,qBAAqBiB,cAAc;QACrD;QAEA,MAAMC,gBAAgB;YACpBvB,aAAa;gBACXwB,WAAWC,QAAQlB,OAAOG,YAAY;YACxC;YACAT,cAAc;gBACZuB,WAAWC,QAAQlB,OAAOG,YAAY;YACxC;SACD;QAED,MAAMgB,SACJrB,qBAAqBiB,cAAc,IACnC,OAAOjB,qBAAqBiB,cAAc,KAAK,aAC3CjB,qBAAqBiB,cAAc,CAAC;YAAEC;QAAc,KACpDA;QAEN,kCAAkC;QAClChB,OAAOW,WAAW,GAAGX,OAAOW,WAAW,IAAI,EAAE;QAE7C,yEAAyE;QACzEX,OAAOW,WAAW,GAAGX,OAAOW,WAAW,CAACP,GAAG,CAAC,CAACgB;YAC3C,IAAIX,aAAaE,WAAW,CAACU,QAAQ,CAACD,iBAAiBE,IAAI,GAAG;gBAC5D,IAAI,CAACF,iBAAiBG,MAAM,EAAE;oBAC5BC,QAAQC,IAAI,CACV,CAAC,gCAAgC,EAAEL,iBAAiBE,IAAI,CAAC,wDAAwD,CAAC;oBAEpH,OAAOF;gBACT;gBAEA,OAAO;oBACL,GAAGA,gBAAgB;oBACnBM,OAAO;wBACL,GAAGN,iBAAiBM,KAAK;wBACzBC,YAAY;4BACV,GAAIP,iBAAiBM,KAAK,EAAEC,cAAc,CAAC,CAAC;4BAC5CC,iBAAiB;mCACXR,iBAAiBM,KAAK,EAAEC,YAAYC,mBAAmB,EAAE;gCAC7D;6BACD;wBACH;oBACF;oBACAT,QAAQ;2BAAKC,iBAAiBD,MAAM,IAAI,EAAE;2BAAMA;qBAAO;gBACzD;YACF;YAEA,OAAOC;QACT;QAEA,OAAO;YACL,GAAGpB,MAAM;YACT6B,QAAQ;gBACN,GAAG7B,OAAO6B,MAAM;gBAChB,gDAAgD;gBAChDC,qBAAqBrB;YACvB;YACAsB,WAAW;mBACL/B,OAAO+B,SAAS,IAAI,EAAE;gBAC1B;oBACEC,MAAM;oBACNC,QAAQ;oBACRC,SAASvC;gBACX;gBACA;oBACEqC,MAAM;oBACNC,QAAQ;oBACRC,SAAStC;gBACX;aACD;QACH;IACF,EAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Field } from 'payload';
|
|
2
|
+
/** Configuration options for the alt text plugin. */
|
|
3
|
+
export type IncomingAltTextPluginConfig = {
|
|
4
|
+
/** Whether the plugin is enabled. */
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
/** OpenAI API key for authentication. */
|
|
7
|
+
openAIApiKey: string;
|
|
8
|
+
/** Collection slugs to enable the plugin for. */
|
|
9
|
+
collections: string[];
|
|
10
|
+
/** Maximum number of concurrent API requests for bulk operations. */
|
|
11
|
+
maxBulkGenerateConcurrency?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Function to get the thumbnail URL of an image document.
|
|
14
|
+
* This URL will be sent to the LLM for analysis.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* - The URL must be publicly accessible so the LLM can fetch it
|
|
18
|
+
* - Use a thumbnail/preview version of the image when possible (e.g. from the sizes field)
|
|
19
|
+
*/
|
|
20
|
+
getImageThumbnail: (doc: Record<string, unknown>) => string;
|
|
21
|
+
/** The OpenAI LLM model to use for alt text generation. */
|
|
22
|
+
model?: 'gpt-4.1-nano' | 'gpt-4.1-mini';
|
|
23
|
+
/** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */
|
|
24
|
+
fieldsOverride?: (args: {
|
|
25
|
+
defaultFields: Field[];
|
|
26
|
+
}) => Field[];
|
|
27
|
+
};
|
|
28
|
+
/** Configuration of the alt text plugin after defaults have been applied. */
|
|
29
|
+
export type AltTextPluginConfig = {
|
|
30
|
+
/** Whether the plugin is enabled. */
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
/** OpenAI API key for authentication. */
|
|
33
|
+
openAIApiKey: string;
|
|
34
|
+
/** Collection slugs to enable the plugin for. */
|
|
35
|
+
collections: string[];
|
|
36
|
+
/** Maximum number of concurrent API requests for bulk generate operations. */
|
|
37
|
+
maxBulkGenerateConcurrency: number;
|
|
38
|
+
/** Function to get the thumbnail URL of an image document. */
|
|
39
|
+
getImageThumbnail: (doc: Record<string, unknown>) => string;
|
|
40
|
+
/** The OpenAI LLM model to use for alt text generation. */
|
|
41
|
+
model: 'gpt-4.1-nano' | 'gpt-4.1-mini';
|
|
42
|
+
/** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */
|
|
43
|
+
fieldsOverride?: (args: {
|
|
44
|
+
defaultFields: Field[];
|
|
45
|
+
}) => Field[];
|
|
46
|
+
/** The locales to generate alt texts for. */
|
|
47
|
+
locales: string[];
|
|
48
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types/AltTextPluginConfig.ts"],"sourcesContent":["import { Field } from 'payload'\n\n/** Configuration options for the alt text plugin. */\nexport type IncomingAltTextPluginConfig = {\n /** Whether the plugin is enabled. */\n enabled?: boolean\n\n /** OpenAI API key for authentication. */\n openAIApiKey: string\n\n /** Collection slugs to enable the plugin for. */\n collections: string[]\n\n /** Maximum number of concurrent API requests for bulk operations. */\n maxBulkGenerateConcurrency?: number\n\n /**\n * Function to get the thumbnail URL of an image document.\n * This URL will be sent to the LLM for analysis.\n *\n * @remarks\n * - The URL must be publicly accessible so the LLM can fetch it\n * - Use a thumbnail/preview version of the image when possible (e.g. from the sizes field)\n */\n getImageThumbnail: (doc: Record<string, unknown>) => string\n\n /** The OpenAI LLM model to use for alt text generation. */\n model?: 'gpt-4.1-nano' | 'gpt-4.1-mini'\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n}\n\n/** Configuration of the alt text plugin after defaults have been applied. */\nexport type AltTextPluginConfig = {\n /** Whether the plugin is enabled. */\n enabled: boolean\n\n /** OpenAI API key for authentication. */\n openAIApiKey: string\n\n /** Collection slugs to enable the plugin for. */\n collections: string[]\n\n /** Maximum number of concurrent API requests for bulk generate operations. */\n maxBulkGenerateConcurrency: number\n\n /** Function to get the thumbnail URL of an image document. */\n getImageThumbnail: (doc: Record<string, unknown>) => string\n\n /** The OpenAI LLM model to use for alt text generation. */\n model: 'gpt-4.1-nano' | 'gpt-4.1-mini'\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /** The locales to generate alt texts for. */\n locales: string[]\n}\n"],"names":[],"mappings":"AAiCA,2EAA2E,GAC3E,WAwBC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type OpenAI from 'openai';
|
|
2
|
+
import type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js';
|
|
3
|
+
type GenerationCost = {
|
|
4
|
+
model: 'gpt-4.1-nano' | 'gpt-4.1-mini';
|
|
5
|
+
inputCost: number;
|
|
6
|
+
outputCost: number;
|
|
7
|
+
totalCost: number;
|
|
8
|
+
totalTokens: number;
|
|
9
|
+
};
|
|
10
|
+
/** Calculates the cost of a generation. */
|
|
11
|
+
export declare function getGenerationCost(response: OpenAI.Chat.Completions.ChatCompletion, model: AltTextPluginConfig['model']): GenerationCost;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Calculates the cost of a generation. */ export function getGenerationCost(response, model) {
|
|
2
|
+
const modelCosts = {
|
|
3
|
+
// see https://platform.openai.com/docs/models/gpt-4.1-nano
|
|
4
|
+
'gpt-4.1-nano': {
|
|
5
|
+
input: 0.1,
|
|
6
|
+
output: 0.4
|
|
7
|
+
},
|
|
8
|
+
// see https://platform.openai.com/docs/models/gpt-4.1-mini
|
|
9
|
+
'gpt-4.1-mini': {
|
|
10
|
+
input: 0.4,
|
|
11
|
+
output: 1.6
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
// Calculate cost based on token usage
|
|
15
|
+
const inputTokens = response.usage?.prompt_tokens || 0;
|
|
16
|
+
const outputTokens = response.usage?.completion_tokens || 0;
|
|
17
|
+
const totalTokens = response.usage?.total_tokens || 0;
|
|
18
|
+
const inputCost = inputTokens / 1_000_000 * modelCosts[model].input;
|
|
19
|
+
const outputCost = outputTokens / 1_000_000 * modelCosts[model].output;
|
|
20
|
+
const totalCost = inputCost + outputCost;
|
|
21
|
+
return {
|
|
22
|
+
model,
|
|
23
|
+
inputCost,
|
|
24
|
+
outputCost,
|
|
25
|
+
totalCost,
|
|
26
|
+
totalTokens
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=getGenerationCost.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/getGenerationCost.ts"],"sourcesContent":["import type OpenAI from 'openai'\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\ntype GenerationCost = {\n model: 'gpt-4.1-nano' | 'gpt-4.1-mini'\n inputCost: number\n outputCost: number\n totalCost: number\n totalTokens: number\n}\n\n/** Calculates the cost of a generation. */\nexport function getGenerationCost(\n response: OpenAI.Chat.Completions.ChatCompletion,\n model: AltTextPluginConfig['model'],\n): GenerationCost {\n const modelCosts: Record<AltTextPluginConfig['model'], { input: number; output: number }> = {\n // see https://platform.openai.com/docs/models/gpt-4.1-nano\n 'gpt-4.1-nano': {\n input: 0.1,\n output: 0.4,\n },\n // see https://platform.openai.com/docs/models/gpt-4.1-mini\n 'gpt-4.1-mini': {\n input: 0.4,\n output: 1.6,\n },\n }\n\n // Calculate cost based on token usage\n const inputTokens = response.usage?.prompt_tokens || 0\n const outputTokens = response.usage?.completion_tokens || 0\n const totalTokens = response.usage?.total_tokens || 0\n\n const inputCost = (inputTokens / 1_000_000) * modelCosts[model].input\n const outputCost = (outputTokens / 1_000_000) * modelCosts[model].output\n const totalCost = inputCost + outputCost\n\n return {\n model,\n inputCost,\n outputCost,\n totalCost,\n totalTokens,\n }\n}\n"],"names":["getGenerationCost","response","model","modelCosts","input","output","inputTokens","usage","prompt_tokens","outputTokens","completion_tokens","totalTokens","total_tokens","inputCost","outputCost","totalCost"],"mappings":"AAWA,yCAAyC,GACzC,OAAO,SAASA,kBACdC,QAAgD,EAChDC,KAAmC;IAEnC,MAAMC,aAAsF;QAC1F,2DAA2D;QAC3D,gBAAgB;YACdC,OAAO;YACPC,QAAQ;QACV;QACA,2DAA2D;QAC3D,gBAAgB;YACdD,OAAO;YACPC,QAAQ;QACV;IACF;IAEA,sCAAsC;IACtC,MAAMC,cAAcL,SAASM,KAAK,EAAEC,iBAAiB;IACrD,MAAMC,eAAeR,SAASM,KAAK,EAAEG,qBAAqB;IAC1D,MAAMC,cAAcV,SAASM,KAAK,EAAEK,gBAAgB;IAEpD,MAAMC,YAAY,AAACP,cAAc,YAAaH,UAAU,CAACD,MAAM,CAACE,KAAK;IACrE,MAAMU,aAAa,AAACL,eAAe,YAAaN,UAAU,CAACD,MAAM,CAACG,MAAM;IACxE,MAAMU,YAAYF,YAAYC;IAE9B,OAAO;QACLZ;QACAW;QACAC;QACAC;QACAJ;IACF;AACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AutoParseableResponseFormat } from 'openai/lib/parser.mjs';
|
|
2
|
+
import { ResponseFormatJSONSchema } from 'openai/resources/shared.mjs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a chat completion `JSONSchema` response format object from
|
|
6
|
+
* the given Zod schema.
|
|
7
|
+
*
|
|
8
|
+
* This is a temporary drop in replacement for the zodResponseFormat from openai/helpers/zod.ts
|
|
9
|
+
* because of issue https://github.com/openai/openai-node/issues/1576
|
|
10
|
+
*/
|
|
11
|
+
export declare function zodResponseFormat<ZodInput extends z.ZodType>(zodObject: ZodInput, name: string, props?: Omit<ResponseFormatJSONSchema.JSONSchema, 'schema' | 'strict' | 'name'>): AutoParseableResponseFormat<z.infer<ZodInput>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { makeParseableResponseFormat } from 'openai/lib/parser.mjs';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a chat completion `JSONSchema` response format object from
|
|
5
|
+
* the given Zod schema.
|
|
6
|
+
*
|
|
7
|
+
* This is a temporary drop in replacement for the zodResponseFormat from openai/helpers/zod.ts
|
|
8
|
+
* because of issue https://github.com/openai/openai-node/issues/1576
|
|
9
|
+
*/ export function zodResponseFormat(zodObject, name, props) {
|
|
10
|
+
return makeParseableResponseFormat({
|
|
11
|
+
type: 'json_schema',
|
|
12
|
+
json_schema: {
|
|
13
|
+
...props,
|
|
14
|
+
name,
|
|
15
|
+
strict: true,
|
|
16
|
+
schema: z.toJSONSchema(zodObject, {
|
|
17
|
+
target: 'draft-7'
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
}, (content)=>zodObject.parse(JSON.parse(content)));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=zodResponseFormat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/zodResponseFormat.ts"],"sourcesContent":["import { AutoParseableResponseFormat, makeParseableResponseFormat } from 'openai/lib/parser.mjs'\nimport { ResponseFormatJSONSchema } from 'openai/resources/shared.mjs'\nimport { z } from 'zod'\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 */\nexport function zodResponseFormat<ZodInput extends z.ZodType>(\n zodObject: ZodInput,\n name: string,\n props?: Omit<ResponseFormatJSONSchema.JSONSchema, 'schema' | 'strict' | 'name'>,\n): AutoParseableResponseFormat<z.infer<ZodInput>> {\n return makeParseableResponseFormat(\n {\n type: 'json_schema',\n json_schema: {\n ...props,\n name,\n strict: true,\n schema: z.toJSONSchema(zodObject, { target: 'draft-7' }),\n },\n },\n (content) => zodObject.parse(JSON.parse(content)),\n )\n}\n"],"names":["makeParseableResponseFormat","z","zodResponseFormat","zodObject","name","props","type","json_schema","strict","schema","toJSONSchema","target","content","parse","JSON"],"mappings":"AAAA,SAAsCA,2BAA2B,QAAQ,wBAAuB;AAEhG,SAASC,CAAC,QAAQ,MAAK;AAEvB;;;;;;CAMC,GACD,OAAO,SAASC,kBACdC,SAAmB,EACnBC,IAAY,EACZC,KAA+E;IAE/E,OAAOL,4BACL;QACEM,MAAM;QACNC,aAAa;YACX,GAAGF,KAAK;YACRD;YACAI,QAAQ;YACRC,QAAQR,EAAES,YAAY,CAACP,WAAW;gBAAEQ,QAAQ;YAAU;QACxD;IACF,GACA,CAACC,UAAYT,UAAUU,KAAK,CAACC,KAAKD,KAAK,CAACD;AAE5C"}
|
package/package.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jhb.software/payload-alt-text-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Payload CMS plugin that adds essential fields for hierarchical page structure to collections.",
|
|
5
|
+
"bugs": "https://github.com/jhb-software/payload-plugins/issues",
|
|
6
|
+
"repository": "https://github.com/jhb-software/payload-plugins",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"payload",
|
|
9
|
+
"plugin",
|
|
10
|
+
"alt-text",
|
|
11
|
+
"accessibility",
|
|
12
|
+
"ai"
|
|
13
|
+
],
|
|
14
|
+
"author": "JHB Software",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./src/index.ts",
|
|
18
|
+
"types": "./src/index.ts",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
21
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
22
|
+
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
23
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
24
|
+
"clean": "rimraf --glob {dist,*.tsbuildinfo}",
|
|
25
|
+
"dev": "tsc -w",
|
|
26
|
+
"format": "prettier --write src",
|
|
27
|
+
"lint": "eslint ./src",
|
|
28
|
+
"lint:fix": "eslint ./src --fix",
|
|
29
|
+
"prepublishOnly": "pnpm clean && pnpm build"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"openai": "^6.6.0",
|
|
33
|
+
"p-map": "^7.0.3",
|
|
34
|
+
"zod": "^4.1.12"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@payloadcms/ui": "^3.60.0",
|
|
38
|
+
"next": "15.5.6",
|
|
39
|
+
"payload": "^3.60.0",
|
|
40
|
+
"react": "19.2.0",
|
|
41
|
+
"react-dom": "19.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@swc/cli": "^0.7.8",
|
|
45
|
+
"@swc/core": "^1.13.3",
|
|
46
|
+
"@types/react": "19.2.2",
|
|
47
|
+
"@types/react-dom": "19.2.2",
|
|
48
|
+
"copyfiles": "2.4.1",
|
|
49
|
+
"prettier": "^3.6.2",
|
|
50
|
+
"rimraf": "6.0.1",
|
|
51
|
+
"typescript": "5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist"
|
|
55
|
+
],
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"main": "./dist/index.js",
|
|
58
|
+
"types": "./dist/index.d.ts",
|
|
59
|
+
"registry": "https://registry.npmjs.org/",
|
|
60
|
+
"access": "public",
|
|
61
|
+
"exports": {
|
|
62
|
+
".": {
|
|
63
|
+
"import": "./dist/index.js",
|
|
64
|
+
"types": "./dist/index.d.ts",
|
|
65
|
+
"default": "./dist/index.js"
|
|
66
|
+
},
|
|
67
|
+
"./client": {
|
|
68
|
+
"import": "./dist/exports/client.js",
|
|
69
|
+
"types": "./dist/exports/client.d.ts",
|
|
70
|
+
"default": "./dist/exports/client.js"
|
|
71
|
+
},
|
|
72
|
+
"./server": {
|
|
73
|
+
"import": "./dist/exports/server.js",
|
|
74
|
+
"types": "./dist/exports/server.d.ts",
|
|
75
|
+
"default": "./dist/exports/server.js"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"exports": {
|
|
80
|
+
".": {
|
|
81
|
+
"import": "./src/index.ts",
|
|
82
|
+
"types": "./src/index.ts",
|
|
83
|
+
"default": "./src/index.ts"
|
|
84
|
+
},
|
|
85
|
+
"./client": {
|
|
86
|
+
"import": "./src/exports/client.ts",
|
|
87
|
+
"types": "./src/exports/client.ts",
|
|
88
|
+
"default": "./src/exports/client.ts"
|
|
89
|
+
},
|
|
90
|
+
"./server": {
|
|
91
|
+
"import": "./src/exports/server.ts",
|
|
92
|
+
"types": "./src/exports/server.ts",
|
|
93
|
+
"default": "./src/exports/server.ts"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"engines": {
|
|
97
|
+
"node": "^18.20.2 || >=20.9.0"
|
|
98
|
+
}
|
|
99
|
+
}
|