@jhb.software/payload-alt-text-plugin 0.1.1 → 0.2.1
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 +5 -3
- package/dist/components/BulkGenerateAltTextsButton.d.ts +3 -1
- package/dist/components/BulkGenerateAltTextsButton.js +16 -10
- package/dist/components/BulkGenerateAltTextsButton.js.map +1 -1
- package/dist/components/GenerateAltTextButton.js +14 -12
- package/dist/components/GenerateAltTextButton.js.map +1 -1
- package/dist/endpoints/bulkGenerateAltTexts.js +22 -2
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +19 -3
- package/dist/endpoints/generateAltText.js.map +1 -1
- package/dist/fields/altTextField.js +9 -7
- package/dist/fields/altTextField.js.map +1 -1
- package/dist/fields/keywordsField.js +4 -3
- package/dist/fields/keywordsField.js.map +1 -1
- package/dist/plugin.js +28 -4
- package/dist/plugin.js.map +1 -1
- package/dist/translations/de.d.ts +2 -0
- package/dist/translations/de.js +33 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.d.ts +2 -0
- package/dist/translations/en.js +33 -0
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/index.d.ts +13 -0
- package/dist/translations/index.js +8 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/translations/translation-schema.json +54 -0
- package/dist/types/AltTextPluginConfig.d.ts +11 -3
- package/dist/types/AltTextPluginConfig.js.map +1 -1
- package/dist/utilities/localesFromConfig.d.ts +3 -0
- package/dist/utilities/localesFromConfig.js +9 -0
- package/dist/utilities/localesFromConfig.js.map +1 -0
- package/dist/utils/deepMergeSimple.d.ts +11 -0
- package/dist/utils/deepMergeSimple.js +30 -0
- package/dist/utils/deepMergeSimple.js.map +1 -0
- package/dist/utils/translatedLabel.d.ts +3 -0
- package/dist/utils/translatedLabel.js +9 -0
- package/dist/utils/translatedLabel.js.map +1 -0
- package/dist/utils/usePluginTranslation.d.ts +5 -0
- package/dist/utils/usePluginTranslation.js +16 -0
- package/dist/utils/usePluginTranslation.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Image Alt Text Generation Plugin for Payload CMS
|
|
2
2
|
|
|
3
|
-
A minimal plugin to generate image alt texts using OpenAI's Vision API.
|
|
3
|
+
A minimal plugin to generate image alt texts using OpenAI's Vision API. This plugin automatically adds an alt text field with a button to generate the alt text to upload collections, and includes a bulk generation feature in the list view for processing multiple images at once.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ import { payloadAltTextPlugin } from '@jhb.software/payload-alt-text-plugin'
|
|
|
18
18
|
export default buildConfig({
|
|
19
19
|
plugins: [
|
|
20
20
|
payloadAltTextPlugin({
|
|
21
|
-
collections: ['media'], // Specify which upload collections
|
|
21
|
+
collections: ['media'], // Specify which upload collections to enable the plugin for
|
|
22
22
|
openAIApiKey: process.env.OPENAI_API_KEY!,
|
|
23
23
|
model: 'gpt-4.1-mini',
|
|
24
24
|
getImageThumbnail: (doc: Record<string, unknown>) => {
|
|
@@ -30,6 +30,8 @@ export default buildConfig({
|
|
|
30
30
|
})
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
Note: When localization is disabled in your Payload config (default), you need to specify the locale to generate the alt texts in via the `locale` plugin option.
|
|
34
|
+
|
|
33
35
|
## Features
|
|
34
36
|
|
|
35
37
|
When the plugin is enabled for an upload collection, it will
|
|
@@ -39,7 +41,7 @@ When the plugin is enabled for an upload collection, it will
|
|
|
39
41
|
- This field will include a description of what the alt text should be
|
|
40
42
|
2. Add a keywords fields to the collection
|
|
41
43
|
- This field will be automatically filled when generating the alt text
|
|
42
|
-
- It
|
|
44
|
+
- It will be used for improving the search of images in the admin panel
|
|
43
45
|
2. Add a bulk generate button to the collection list view
|
|
44
46
|
- This button will allow you to generate alt text for multiple images at once
|
|
45
47
|
|
|
@@ -5,34 +5,39 @@ import { useRouter } from 'next/navigation.js';
|
|
|
5
5
|
import { useTransition } from 'react';
|
|
6
6
|
import { Lightning } from './icons/Lightning.js';
|
|
7
7
|
import { Spinner } from './icons/Spinner.js';
|
|
8
|
-
|
|
8
|
+
import { usePluginTranslation } from '../utils/usePluginTranslation.js';
|
|
9
|
+
export function BulkGenerateAltTextsButton({ collectionSlug }) {
|
|
10
|
+
const { t } = usePluginTranslation();
|
|
9
11
|
const [isPending, startTransition] = useTransition();
|
|
10
12
|
const { selected, setSelection } = useSelection();
|
|
11
13
|
const selectedIds = Array.from(selected.entries()).filter(([, isSelected])=>isSelected).map(([id])=>id);
|
|
12
14
|
const router = useRouter();
|
|
13
15
|
const handleGenerateAltTexts = async ()=>{
|
|
14
16
|
startTransition(async ()=>{
|
|
17
|
+
if (!collectionSlug) {
|
|
18
|
+
throw new Error('Collection slug is required');
|
|
19
|
+
}
|
|
15
20
|
try {
|
|
16
21
|
const response = await fetch('/api/alt-text-plugin/bulk-generate-alt-texts', {
|
|
17
22
|
method: 'POST',
|
|
18
23
|
body: JSON.stringify({
|
|
19
|
-
collection:
|
|
24
|
+
collection: collectionSlug,
|
|
20
25
|
ids: selectedIds
|
|
21
26
|
})
|
|
22
27
|
});
|
|
23
28
|
if (!response.ok) {
|
|
24
|
-
toast.error('
|
|
29
|
+
toast.error(t('failedToGenerate'));
|
|
25
30
|
return;
|
|
26
31
|
}
|
|
27
32
|
const data = await response.json();
|
|
28
33
|
if (data.erroredDocs.length > 0) {
|
|
29
|
-
toast.error(
|
|
34
|
+
toast.error(t('failedToGenerateForXImages').replace('{X}', data.erroredDocs.length.toString()));
|
|
30
35
|
}
|
|
31
36
|
// in case not all images were updated, show a warning instead of a success message:
|
|
32
37
|
if (data.updatedDocs === data.totalDocs) {
|
|
33
|
-
toast.success(
|
|
38
|
+
toast.success(t('xOfYImagesUpdated').replace('{X}', data.updatedDocs.toString()).replace('{Y}', data.totalDocs.toString()));
|
|
34
39
|
} else {
|
|
35
|
-
toast.warning(
|
|
40
|
+
toast.warning(t('xOfYImagesUpdated').replace('{X}', data.updatedDocs.toString()).replace('{Y}', data.totalDocs.toString()));
|
|
36
41
|
}
|
|
37
42
|
// deselect all previously selected images
|
|
38
43
|
for (const id of selectedIds){
|
|
@@ -41,7 +46,7 @@ export function BulkGenerateAltTextsButton() {
|
|
|
41
46
|
router.refresh();
|
|
42
47
|
} catch (error) {
|
|
43
48
|
console.error('Error generating alt text:', error);
|
|
44
|
-
toast.error('
|
|
49
|
+
toast.error(t('errorGeneratingAltText'));
|
|
45
50
|
}
|
|
46
51
|
});
|
|
47
52
|
};
|
|
@@ -57,10 +62,11 @@ export function BulkGenerateAltTextsButton() {
|
|
|
57
62
|
icon: isPending ? /*#__PURE__*/ _jsx(Spinner, {}) : /*#__PURE__*/ _jsx(Lightning, {}),
|
|
58
63
|
className: "m-0",
|
|
59
64
|
children: [
|
|
60
|
-
|
|
61
|
-
selectedIds.length,
|
|
65
|
+
t('generateAltTextFor'),
|
|
62
66
|
" ",
|
|
63
|
-
selectedIds.length
|
|
67
|
+
selectedIds.length,
|
|
68
|
+
' ',
|
|
69
|
+
selectedIds.length === 1 ? t('image') : t('images')
|
|
64
70
|
]
|
|
65
71
|
})
|
|
66
72
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/BulkGenerateAltTextsButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useSelection } from '@payloadcms/ui'\nimport { useRouter } from 'next/navigation.js'\nimport { useTransition } from 'react'\n\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function BulkGenerateAltTextsButton() {\n const [isPending, startTransition] = useTransition()\n const { selected, setSelection } = useSelection()\n\n const selectedIds = Array.from(selected.entries())\n .filter(([, isSelected]) => isSelected)\n .map(([id]) => id) as string[]\n\n const router = useRouter()\n\n const handleGenerateAltTexts = async () => {\n startTransition(async () => {\n try {\n const response = await fetch('/api/alt-text-plugin/bulk-generate-alt-texts', {\n method: 'POST',\n body: JSON.stringify({\n collection:
|
|
1
|
+
{"version":3,"sources":["../../src/components/BulkGenerateAltTextsButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useSelection } from '@payloadcms/ui'\nimport { useRouter } from 'next/navigation.js'\nimport { useTransition } from 'react'\n\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\nimport { usePluginTranslation } from '../utils/usePluginTranslation.js'\n\nexport function BulkGenerateAltTextsButton({ collectionSlug }: { collectionSlug: string }) {\n const { t } = usePluginTranslation()\n const [isPending, startTransition] = useTransition()\n const { selected, setSelection } = useSelection()\n\n const selectedIds = Array.from(selected.entries())\n .filter(([, isSelected]) => isSelected)\n .map(([id]) => id) as string[]\n\n const router = useRouter()\n\n const handleGenerateAltTexts = async () => {\n startTransition(async () => {\n if (!collectionSlug) {\n throw new Error('Collection slug is required')\n }\n\n try {\n const response = await fetch('/api/alt-text-plugin/bulk-generate-alt-texts', {\n method: 'POST',\n body: JSON.stringify({\n collection: collectionSlug,\n ids: selectedIds,\n }),\n })\n\n if (!response.ok) {\n toast.error(t('failedToGenerate'))\n return\n }\n\n const data = (await response.json()) as {\n updatedDocs: number\n totalDocs: number\n erroredDocs: string[]\n }\n\n if (data.erroredDocs.length > 0) {\n toast.error(\n t('failedToGenerateForXImages').replace('{X}', data.erroredDocs.length.toString()),\n )\n }\n\n // in case not all images were updated, show a warning instead of a success message:\n if (data.updatedDocs === data.totalDocs) {\n toast.success(\n t('xOfYImagesUpdated')\n .replace('{X}', data.updatedDocs.toString())\n .replace('{Y}', data.totalDocs.toString()),\n )\n } else {\n toast.warning(\n t('xOfYImagesUpdated')\n .replace('{X}', data.updatedDocs.toString())\n .replace('{Y}', data.totalDocs.toString()),\n )\n }\n\n // deselect all previously selected images\n for (const id of selectedIds) {\n setSelection(id)\n }\n\n router.refresh()\n } catch (error) {\n console.error('Error generating alt text:', error)\n toast.error(t('errorGeneratingAltText'))\n }\n })\n }\n\n return (\n selectedIds.length > 0 && (\n <div style={{ display: 'flex', justifyContent: 'right' }} className=\"m-0\">\n <Button\n onClick={handleGenerateAltTexts}\n disabled={isPending || selectedIds.length === 0}\n icon={isPending ? <Spinner /> : <Lightning />}\n className=\"m-0\"\n >\n {t('generateAltTextFor')} {selectedIds.length}{' '}\n {selectedIds.length === 1 ? t('image') : t('images')}\n </Button>\n </div>\n )\n )\n}\n"],"names":["Button","toast","useSelection","useRouter","useTransition","Lightning","Spinner","usePluginTranslation","BulkGenerateAltTextsButton","collectionSlug","t","isPending","startTransition","selected","setSelection","selectedIds","Array","from","entries","filter","isSelected","map","id","router","handleGenerateAltTexts","Error","response","fetch","method","body","JSON","stringify","collection","ids","ok","error","data","json","erroredDocs","length","replace","toString","updatedDocs","totalDocs","success","warning","refresh","console","div","style","display","justifyContent","className","onClick","disabled","icon"],"mappings":"AAAA;;AAEA,SAASA,MAAM,EAAEC,KAAK,EAAEC,YAAY,QAAQ,iBAAgB;AAC5D,SAASC,SAAS,QAAQ,qBAAoB;AAC9C,SAASC,aAAa,QAAQ,QAAO;AAErC,SAASC,SAAS,QAAQ,uBAAsB;AAChD,SAASC,OAAO,QAAQ,qBAAoB;AAC5C,SAASC,oBAAoB,QAAQ,mCAAkC;AAEvE,OAAO,SAASC,2BAA2B,EAAEC,cAAc,EAA8B;IACvF,MAAM,EAAEC,CAAC,EAAE,GAAGH;IACd,MAAM,CAACI,WAAWC,gBAAgB,GAAGR;IACrC,MAAM,EAAES,QAAQ,EAAEC,YAAY,EAAE,GAAGZ;IAEnC,MAAMa,cAAcC,MAAMC,IAAI,CAACJ,SAASK,OAAO,IAC5CC,MAAM,CAAC,CAAC,GAAGC,WAAW,GAAKA,YAC3BC,GAAG,CAAC,CAAC,CAACC,GAAG,GAAKA;IAEjB,MAAMC,SAASpB;IAEf,MAAMqB,yBAAyB;QAC7BZ,gBAAgB;YACd,IAAI,CAACH,gBAAgB;gBACnB,MAAM,IAAIgB,MAAM;YAClB;YAEA,IAAI;gBACF,MAAMC,WAAW,MAAMC,MAAM,gDAAgD;oBAC3EC,QAAQ;oBACRC,MAAMC,KAAKC,SAAS,CAAC;wBACnBC,YAAYvB;wBACZwB,KAAKlB;oBACP;gBACF;gBAEA,IAAI,CAACW,SAASQ,EAAE,EAAE;oBAChBjC,MAAMkC,KAAK,CAACzB,EAAE;oBACd;gBACF;gBAEA,MAAM0B,OAAQ,MAAMV,SAASW,IAAI;gBAMjC,IAAID,KAAKE,WAAW,CAACC,MAAM,GAAG,GAAG;oBAC/BtC,MAAMkC,KAAK,CACTzB,EAAE,8BAA8B8B,OAAO,CAAC,OAAOJ,KAAKE,WAAW,CAACC,MAAM,CAACE,QAAQ;gBAEnF;gBAEA,oFAAoF;gBACpF,IAAIL,KAAKM,WAAW,KAAKN,KAAKO,SAAS,EAAE;oBACvC1C,MAAM2C,OAAO,CACXlC,EAAE,qBACC8B,OAAO,CAAC,OAAOJ,KAAKM,WAAW,CAACD,QAAQ,IACxCD,OAAO,CAAC,OAAOJ,KAAKO,SAAS,CAACF,QAAQ;gBAE7C,OAAO;oBACLxC,MAAM4C,OAAO,CACXnC,EAAE,qBACC8B,OAAO,CAAC,OAAOJ,KAAKM,WAAW,CAACD,QAAQ,IACxCD,OAAO,CAAC,OAAOJ,KAAKO,SAAS,CAACF,QAAQ;gBAE7C;gBAEA,0CAA0C;gBAC1C,KAAK,MAAMnB,MAAMP,YAAa;oBAC5BD,aAAaQ;gBACf;gBAEAC,OAAOuB,OAAO;YAChB,EAAE,OAAOX,OAAO;gBACdY,QAAQZ,KAAK,CAAC,8BAA8BA;gBAC5ClC,MAAMkC,KAAK,CAACzB,EAAE;YAChB;QACF;IACF;IAEA,OACEK,YAAYwB,MAAM,GAAG,mBACnB,KAACS;QAAIC,OAAO;YAAEC,SAAS;YAAQC,gBAAgB;QAAQ;QAAGC,WAAU;kBAClE,cAAA,MAACpD;YACCqD,SAAS7B;YACT8B,UAAU3C,aAAaI,YAAYwB,MAAM,KAAK;YAC9CgB,MAAM5C,0BAAY,KAACL,6BAAa,KAACD;YACjC+C,WAAU;;gBAET1C,EAAE;gBAAsB;gBAAEK,YAAYwB,MAAM;gBAAE;gBAC9CxB,YAAYwB,MAAM,KAAK,IAAI7B,EAAE,WAAWA,EAAE;;;;AAKrD"}
|
|
@@ -4,7 +4,9 @@ import { Button, toast, useDocumentInfo, useField, useLocale } from '@payloadcms
|
|
|
4
4
|
import { useTransition } from 'react';
|
|
5
5
|
import { Lightning } from './icons/Lightning.js';
|
|
6
6
|
import { Spinner } from './icons/Spinner.js';
|
|
7
|
+
import { usePluginTranslation } from '../utils/usePluginTranslation.js';
|
|
7
8
|
export function GenerateAltTextButton() {
|
|
9
|
+
const { t } = usePluginTranslation();
|
|
8
10
|
const { id, collectionSlug } = useDocumentInfo();
|
|
9
11
|
const locale = useLocale();
|
|
10
12
|
const [isPending, startTransition] = useTransition();
|
|
@@ -16,7 +18,7 @@ export function GenerateAltTextButton() {
|
|
|
16
18
|
});
|
|
17
19
|
const handleGenerateAltText = async ()=>{
|
|
18
20
|
if (!collectionSlug || !id) {
|
|
19
|
-
toast.error('
|
|
21
|
+
toast.error(t('cannotGenerateMissingFields'));
|
|
20
22
|
throw new Error('Missing required fields');
|
|
21
23
|
}
|
|
22
24
|
startTransition(async ()=>{
|
|
@@ -26,11 +28,11 @@ export function GenerateAltTextButton() {
|
|
|
26
28
|
body: JSON.stringify({
|
|
27
29
|
collection: collectionSlug,
|
|
28
30
|
id: id,
|
|
29
|
-
locale: locale
|
|
31
|
+
locale: locale?.code ?? null
|
|
30
32
|
})
|
|
31
33
|
});
|
|
32
34
|
if (!response.ok) {
|
|
33
|
-
let errorMessage = '
|
|
35
|
+
let errorMessage = t('failedToGenerate');
|
|
34
36
|
try {
|
|
35
37
|
const errorData = await response.json();
|
|
36
38
|
errorMessage = errorData.error;
|
|
@@ -44,13 +46,13 @@ export function GenerateAltTextButton() {
|
|
|
44
46
|
if (data.altText && data.keywords) {
|
|
45
47
|
setAltText(data.altText);
|
|
46
48
|
setKeywords(data.keywords);
|
|
47
|
-
toast.success('
|
|
49
|
+
toast.success(t('altTextGeneratedSuccess'));
|
|
48
50
|
} else {
|
|
49
|
-
toast.error('
|
|
51
|
+
toast.error(t('noAltTextGenerated'));
|
|
50
52
|
}
|
|
51
53
|
} catch (error) {
|
|
52
54
|
console.error('Error generating alt text:', error);
|
|
53
|
-
toast.error('
|
|
55
|
+
toast.error(t('errorGeneratingAltText'));
|
|
54
56
|
}
|
|
55
57
|
});
|
|
56
58
|
};
|
|
@@ -68,7 +70,7 @@ export function GenerateAltTextButton() {
|
|
|
68
70
|
},
|
|
69
71
|
children: [
|
|
70
72
|
/*#__PURE__*/ _jsx("p", {
|
|
71
|
-
children:
|
|
73
|
+
children: t('altTextDescription')
|
|
72
74
|
}),
|
|
73
75
|
/*#__PURE__*/ _jsxs("ol", {
|
|
74
76
|
style: {
|
|
@@ -77,13 +79,13 @@ export function GenerateAltTextButton() {
|
|
|
77
79
|
},
|
|
78
80
|
children: [
|
|
79
81
|
/*#__PURE__*/ _jsx("li", {
|
|
80
|
-
children:
|
|
82
|
+
children: t('altTextRequirement1')
|
|
81
83
|
}),
|
|
82
84
|
/*#__PURE__*/ _jsx("li", {
|
|
83
|
-
children:
|
|
85
|
+
children: t('altTextRequirement2')
|
|
84
86
|
}),
|
|
85
87
|
/*#__PURE__*/ _jsx("li", {
|
|
86
|
-
children: '
|
|
88
|
+
children: t('altTextRequirement3')
|
|
87
89
|
})
|
|
88
90
|
]
|
|
89
91
|
})
|
|
@@ -98,8 +100,8 @@ export function GenerateAltTextButton() {
|
|
|
98
100
|
onClick: handleGenerateAltText,
|
|
99
101
|
disabled: isPending || !id,
|
|
100
102
|
icon: isPending ? /*#__PURE__*/ _jsx(Spinner, {}) : /*#__PURE__*/ _jsx(Lightning, {}),
|
|
101
|
-
tooltip: !id ? '
|
|
102
|
-
children:
|
|
103
|
+
tooltip: !id ? t('pleaseSaveDocumentFirst') : undefined,
|
|
104
|
+
children: t('generateAltText')
|
|
103
105
|
})
|
|
104
106
|
})
|
|
105
107
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/GenerateAltTextButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useDocumentInfo, useField, useLocale } from '@payloadcms/ui'\nimport { useTransition } from 'react'\n\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function GenerateAltTextButton() {\n const { id, collectionSlug } = useDocumentInfo()\n const locale = useLocale()\n const [isPending, startTransition] = useTransition()\n\n const { setValue: setKeywords } = useField<string>({ path: 'keywords' })\n const { setValue: setAltText } = useField<string>({ path: 'alt' })\n\n const handleGenerateAltText = async () => {\n if (!collectionSlug || !id) {\n toast.error('
|
|
1
|
+
{"version":3,"sources":["../../src/components/GenerateAltTextButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useDocumentInfo, useField, useLocale } from '@payloadcms/ui'\nimport { useTransition } from 'react'\n\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\nimport { usePluginTranslation } from '../utils/usePluginTranslation.js'\n\nexport function GenerateAltTextButton() {\n const { t } = usePluginTranslation()\n const { id, collectionSlug } = useDocumentInfo()\n const locale = useLocale()\n const [isPending, startTransition] = useTransition()\n\n const { setValue: setKeywords } = useField<string>({ path: 'keywords' })\n const { setValue: setAltText } = useField<string>({ path: 'alt' })\n\n const handleGenerateAltText = async () => {\n if (!collectionSlug || !id) {\n toast.error(t('cannotGenerateMissingFields'))\n throw new Error('Missing required fields')\n }\n\n startTransition(async () => {\n try {\n const response = await fetch('/api/alt-text-plugin/generate-alt-text', {\n method: 'POST',\n body: JSON.stringify({\n collection: collectionSlug,\n id: id as string,\n locale: locale?.code ?? null, // sent null when localization is disabled\n }),\n })\n\n if (!response.ok) {\n let errorMessage = t('failedToGenerate')\n try {\n const errorData = (await response.json()) as { error: string }\n errorMessage = errorData.error\n } catch (error) {\n console.error('Error generating alt text:', error)\n }\n\n toast.error(errorMessage)\n return\n }\n\n const data = (await response.json()) as {\n altText: string\n keywords: string[]\n }\n\n if (data.altText && data.keywords) {\n setAltText(data.altText)\n setKeywords(data.keywords)\n toast.success(t('altTextGeneratedSuccess'))\n } else {\n toast.error(t('noAltTextGenerated'))\n }\n } catch (error) {\n console.error('Error generating alt text:', error)\n toast.error(t('errorGeneratingAltText'))\n }\n })\n }\n\n return (\n <div style={{ display: 'flex', gap: '20px', marginTop: '10px' }}>\n <div style={{ flex: '1', color: 'var(--theme-elevation-400)' }}>\n <p>{t('altTextDescription')}</p>\n <ol style={{ paddingLeft: '20px', margin: '10px 0' }}>\n <li>{t('altTextRequirement1')}</li>\n <li>{t('altTextRequirement2')}</li>\n <li>{t('altTextRequirement3')}</li>\n </ol>\n </div>\n <div style={{ display: 'flex', alignItems: 'center' }}>\n <Button\n onClick={handleGenerateAltText}\n disabled={isPending || !id}\n icon={isPending ? <Spinner /> : <Lightning />}\n tooltip={!id ? t('pleaseSaveDocumentFirst') : undefined}\n >\n {t('generateAltText')}\n </Button>\n </div>\n </div>\n )\n}\n"],"names":["Button","toast","useDocumentInfo","useField","useLocale","useTransition","Lightning","Spinner","usePluginTranslation","GenerateAltTextButton","t","id","collectionSlug","locale","isPending","startTransition","setValue","setKeywords","path","setAltText","handleGenerateAltText","error","Error","response","fetch","method","body","JSON","stringify","collection","code","ok","errorMessage","errorData","json","console","data","altText","keywords","success","div","style","display","gap","marginTop","flex","color","p","ol","paddingLeft","margin","li","alignItems","onClick","disabled","icon","tooltip","undefined"],"mappings":"AAAA;;AAEA,SAASA,MAAM,EAAEC,KAAK,EAAEC,eAAe,EAAEC,QAAQ,EAAEC,SAAS,QAAQ,iBAAgB;AACpF,SAASC,aAAa,QAAQ,QAAO;AAErC,SAASC,SAAS,QAAQ,uBAAsB;AAChD,SAASC,OAAO,QAAQ,qBAAoB;AAC5C,SAASC,oBAAoB,QAAQ,mCAAkC;AAEvE,OAAO,SAASC;IACd,MAAM,EAAEC,CAAC,EAAE,GAAGF;IACd,MAAM,EAAEG,EAAE,EAAEC,cAAc,EAAE,GAAGV;IAC/B,MAAMW,SAAST;IACf,MAAM,CAACU,WAAWC,gBAAgB,GAAGV;IAErC,MAAM,EAAEW,UAAUC,WAAW,EAAE,GAAGd,SAAiB;QAAEe,MAAM;IAAW;IACtE,MAAM,EAAEF,UAAUG,UAAU,EAAE,GAAGhB,SAAiB;QAAEe,MAAM;IAAM;IAEhE,MAAME,wBAAwB;QAC5B,IAAI,CAACR,kBAAkB,CAACD,IAAI;YAC1BV,MAAMoB,KAAK,CAACX,EAAE;YACd,MAAM,IAAIY,MAAM;QAClB;QAEAP,gBAAgB;YACd,IAAI;gBACF,MAAMQ,WAAW,MAAMC,MAAM,0CAA0C;oBACrEC,QAAQ;oBACRC,MAAMC,KAAKC,SAAS,CAAC;wBACnBC,YAAYjB;wBACZD,IAAIA;wBACJE,QAAQA,QAAQiB,QAAQ;oBAC1B;gBACF;gBAEA,IAAI,CAACP,SAASQ,EAAE,EAAE;oBAChB,IAAIC,eAAetB,EAAE;oBACrB,IAAI;wBACF,MAAMuB,YAAa,MAAMV,SAASW,IAAI;wBACtCF,eAAeC,UAAUZ,KAAK;oBAChC,EAAE,OAAOA,OAAO;wBACdc,QAAQd,KAAK,CAAC,8BAA8BA;oBAC9C;oBAEApB,MAAMoB,KAAK,CAACW;oBACZ;gBACF;gBAEA,MAAMI,OAAQ,MAAMb,SAASW,IAAI;gBAKjC,IAAIE,KAAKC,OAAO,IAAID,KAAKE,QAAQ,EAAE;oBACjCnB,WAAWiB,KAAKC,OAAO;oBACvBpB,YAAYmB,KAAKE,QAAQ;oBACzBrC,MAAMsC,OAAO,CAAC7B,EAAE;gBAClB,OAAO;oBACLT,MAAMoB,KAAK,CAACX,EAAE;gBAChB;YACF,EAAE,OAAOW,OAAO;gBACdc,QAAQd,KAAK,CAAC,8BAA8BA;gBAC5CpB,MAAMoB,KAAK,CAACX,EAAE;YAChB;QACF;IACF;IAEA,qBACE,MAAC8B;QAAIC,OAAO;YAAEC,SAAS;YAAQC,KAAK;YAAQC,WAAW;QAAO;;0BAC5D,MAACJ;gBAAIC,OAAO;oBAAEI,MAAM;oBAAKC,OAAO;gBAA6B;;kCAC3D,KAACC;kCAAGrC,EAAE;;kCACN,MAACsC;wBAAGP,OAAO;4BAAEQ,aAAa;4BAAQC,QAAQ;wBAAS;;0CACjD,KAACC;0CAAIzC,EAAE;;0CACP,KAACyC;0CAAIzC,EAAE;;0CACP,KAACyC;0CAAIzC,EAAE;;;;;;0BAGX,KAAC8B;gBAAIC,OAAO;oBAAEC,SAAS;oBAAQU,YAAY;gBAAS;0BAClD,cAAA,KAACpD;oBACCqD,SAASjC;oBACTkC,UAAUxC,aAAa,CAACH;oBACxB4C,MAAMzC,0BAAY,KAACP,6BAAa,KAACD;oBACjCkD,SAAS,CAAC7C,KAAKD,EAAE,6BAA6B+C;8BAE7C/C,EAAE;;;;;AAKb"}
|
|
@@ -3,6 +3,7 @@ import pMap from 'p-map';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { getGenerationCost } from '../utilities/getGenerationCost.js';
|
|
5
5
|
import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
|
|
6
|
+
import { localesFromConfig } from '../utilities/localesFromConfig.js';
|
|
6
7
|
/**
|
|
7
8
|
* Generates and updates alt text for multiple images in all locales.
|
|
8
9
|
*/ export const bulkGenerateAltTextsEndpoint = async (req)=>{
|
|
@@ -24,7 +25,14 @@ import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
|
|
|
24
25
|
const erroredDocs = [];
|
|
25
26
|
// Get plugin config from payload config
|
|
26
27
|
const pluginConfig = req.payload.config.custom?.altTextPluginConfig;
|
|
27
|
-
if (!pluginConfig
|
|
28
|
+
if (!pluginConfig) {
|
|
29
|
+
return Response.json({
|
|
30
|
+
error: 'Plugin config not found'
|
|
31
|
+
}, {
|
|
32
|
+
status: 500
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!pluginConfig.openAIApiKey) {
|
|
28
36
|
return Response.json({
|
|
29
37
|
error: 'OpenAI API key not configured'
|
|
30
38
|
}, {
|
|
@@ -33,6 +41,18 @@ import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
|
|
|
33
41
|
}
|
|
34
42
|
// Use concurrency from config
|
|
35
43
|
const concurrency = pluginConfig.maxBulkGenerateConcurrency || 16;
|
|
44
|
+
// determine target locales based on config
|
|
45
|
+
const locales = localesFromConfig(req.payload.config);
|
|
46
|
+
const targetLocales = locales ?? [
|
|
47
|
+
pluginConfig.locale
|
|
48
|
+
];
|
|
49
|
+
if (!targetLocales) {
|
|
50
|
+
return Response.json({
|
|
51
|
+
error: 'Could not determine target locales for alt text generation. Please check your plugin configuration.'
|
|
52
|
+
}, {
|
|
53
|
+
status: 500
|
|
54
|
+
});
|
|
55
|
+
}
|
|
36
56
|
await pMap(ids, async (id)=>{
|
|
37
57
|
try {
|
|
38
58
|
await generateAndUpdateAltText({
|
|
@@ -40,7 +60,7 @@ import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
|
|
|
40
60
|
id,
|
|
41
61
|
collection,
|
|
42
62
|
pluginConfig,
|
|
43
|
-
locales:
|
|
63
|
+
locales: targetLocales
|
|
44
64
|
});
|
|
45
65
|
updatedDocs++;
|
|
46
66
|
console.log(`${updatedDocs}/${ids.length} updated (${Math.round(updatedDocs / ids.length * 100)}%)`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { ChatCompletionContentPartText } from 'openai/resources/chat/completions.mjs'\nimport pMap from 'p-map'\nimport type { BasePayload, CollectionSlug, 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 and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint: 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 schema = z.object({\n collection: z.string(),\n ids: z.array(z.string()),\n })\n\n const { collection, ids } = schema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: 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?.openAIApiKey) {\n return Response.json({ error: 'OpenAI API key not configured' }, { status: 500 })\n }\n\n // Use concurrency from config\n const concurrency = pluginConfig.maxBulkGenerateConcurrency || 16\n\n await pMap(\n ids,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n payload: req.payload,\n id,\n collection,\n pluginConfig,\n locales: pluginConfig.locales,\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 updatedDocs,\n totalDocs: ids.length,\n erroredDocs,\n })\n } catch (error) {\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 payload,\n id,\n collection,\n pluginConfig,\n locales,\n}: {\n payload: BasePayload\n id: string\n collection: CollectionSlug\n pluginConfig: AltTextPluginConfig\n locales: string[]\n}) {\n const imageDoc = await payload.findByID({\n collection: collection,\n id: id as string,\n depth: 0,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const openai = new OpenAI({\n apiKey: pluginConfig.openAIApiKey,\n })\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.array(z.string()).describe('Keywords that describe the content of the image'),\n }),\n ]),\n ),\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 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\", \"keywords\" and \"slug\".\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 max_completion_tokens: 300,\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 throw new Error('No result from OpenAI')\n }\n\n for (const locale of locales) {\n await payload.update({\n collection: collection as CollectionSlug,\n id: id as string,\n locale: locale,\n data: {\n alt: (result as any)[locale]?.altText,\n keywords: (result as any)[locale]?.keywords,\n },\n })\n }\n}\n"],"names":["OpenAI","pMap","z","getGenerationCost","zodResponseFormat","bulkGenerateAltTextsEndpoint","req","user","Response","json","error","status","data","schema","object","collection","string","ids","array","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","openAIApiKey","concurrency","maxBulkGenerateConcurrency","id","generateAndUpdateAltText","locales","console","log","length","Math","round","push","join","totalDocs","Error","message","imageDoc","findByID","depth","imageThumbnailUrl","getImageThumbnail","openai","apiKey","modelResponseSchema","Object","fromEntries","map","locale","altText","describe","keywords","response","chat","completions","model","messages","role","content","type","image_url","url","filename","text","max_completion_tokens","response_format","imageId","result","choices","parsed","update","alt"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { ChatCompletionContentPartText } from 'openai/resources/chat/completions.mjs'\nimport pMap from 'p-map'\nimport type { BasePayload, CollectionSlug, 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'\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\n\n/**\n * Generates and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint: 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 schema = z.object({\n collection: z.string(),\n ids: z.array(z.string()),\n })\n\n const { collection, ids } = schema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: 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.openAIApiKey) {\n return Response.json({ error: 'OpenAI API key not configured' }, { status: 500 })\n }\n\n // Use concurrency from config\n const concurrency = pluginConfig.maxBulkGenerateConcurrency || 16\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 payload: req.payload,\n id,\n collection,\n pluginConfig,\n locales: targetLocales,\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 updatedDocs,\n totalDocs: ids.length,\n erroredDocs,\n })\n } catch (error) {\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 payload,\n id,\n collection,\n pluginConfig,\n locales,\n}: {\n payload: BasePayload\n id: string\n collection: CollectionSlug\n pluginConfig: AltTextPluginConfig\n locales: string[]\n}) {\n const imageDoc = await payload.findByID({\n collection: collection,\n id: id as string,\n depth: 0,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const openai = new OpenAI({\n apiKey: pluginConfig.openAIApiKey,\n })\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.array(z.string()).describe('Keywords that describe the content of the image'),\n }),\n ]),\n ),\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 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\", \"keywords\" and \"slug\".\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 max_completion_tokens: 300,\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 throw new Error('No result from OpenAI')\n }\n\n for (const locale of locales) {\n await payload.update({\n collection: collection as CollectionSlug,\n id: id as string,\n locale: locale,\n data: {\n alt: (result as any)[locale]?.altText,\n keywords: (result as any)[locale]?.keywords,\n },\n })\n }\n}\n"],"names":["OpenAI","pMap","z","getGenerationCost","zodResponseFormat","localesFromConfig","bulkGenerateAltTextsEndpoint","req","user","Response","json","error","status","data","schema","object","collection","string","ids","array","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","openAIApiKey","concurrency","maxBulkGenerateConcurrency","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","length","Math","round","push","join","totalDocs","Error","message","imageDoc","findByID","depth","imageThumbnailUrl","getImageThumbnail","openai","apiKey","modelResponseSchema","Object","fromEntries","map","altText","describe","keywords","response","chat","completions","model","messages","role","content","type","image_url","url","filename","text","max_completion_tokens","response_format","imageId","result","choices","parsed","update","alt"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
|
|
|
18
18
|
const requestSchema = z.object({
|
|
19
19
|
collection: z.string(),
|
|
20
20
|
id: z.string(),
|
|
21
|
-
locale: z.string()
|
|
21
|
+
locale: z.string().nullable()
|
|
22
22
|
});
|
|
23
23
|
const { collection, id, locale } = requestSchema.parse(data);
|
|
24
24
|
const imageDoc = await req.payload.findByID({
|
|
@@ -34,13 +34,29 @@ import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
const pluginConfig = req.payload.config.custom?.altTextPluginConfig;
|
|
37
|
-
if (!pluginConfig
|
|
37
|
+
if (!pluginConfig) {
|
|
38
|
+
return Response.json({
|
|
39
|
+
error: 'Plugin config not found'
|
|
40
|
+
}, {
|
|
41
|
+
status: 500
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!pluginConfig.getImageThumbnail) {
|
|
38
45
|
return Response.json({
|
|
39
46
|
error: 'getImageThumbnail function not configured'
|
|
40
47
|
}, {
|
|
41
48
|
status: 500
|
|
42
49
|
});
|
|
43
50
|
}
|
|
51
|
+
// determine target locale
|
|
52
|
+
const targetLocale = locale ?? pluginConfig.locale;
|
|
53
|
+
if (!targetLocale) {
|
|
54
|
+
return Response.json({
|
|
55
|
+
error: 'Could not determine target locale for alt text generation. Please check your plugin configuration.'
|
|
56
|
+
}, {
|
|
57
|
+
status: 500
|
|
58
|
+
});
|
|
59
|
+
}
|
|
44
60
|
const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc);
|
|
45
61
|
if (!imageThumbnailUrl) {
|
|
46
62
|
return Response.json({
|
|
@@ -77,7 +93,7 @@ import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
|
|
|
77
93
|
|
|
78
94
|
If a context is provided, use it to enhance the alt text.
|
|
79
95
|
|
|
80
|
-
Format your response as a JSON object. You must respond in the ${
|
|
96
|
+
Format your response as a JSON object. You must respond in the ${targetLocale} language.
|
|
81
97
|
`
|
|
82
98
|
},
|
|
83
99
|
{
|
|
@@ -1 +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
|
|
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().nullable(),\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) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n if (!pluginConfig.getImageThumbnail) {\n return Response.json({ error: 'getImageThumbnail function not configured' }, { status: 500 })\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 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 ${targetLocale} 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","nullable","parse","imageDoc","payload","findByID","depth","pluginConfig","config","custom","altTextPluginConfig","getImageThumbnail","targetLocale","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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"}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
import { translatedLabel } from '../utils/translatedLabel.js';
|
|
1
2
|
export function altTextField({ localized }) {
|
|
2
3
|
return {
|
|
3
4
|
name: 'alt',
|
|
4
|
-
label: '
|
|
5
|
+
label: translatedLabel('alternateText'),
|
|
5
6
|
type: 'textarea',
|
|
6
7
|
required: true,
|
|
7
8
|
localized: localized,
|
|
8
|
-
validate: (value,
|
|
9
|
-
// if the document has an id, the alt text is required
|
|
10
|
-
if (
|
|
9
|
+
validate: (value, { id, req: { t } })=>{
|
|
10
|
+
// if the document has an id, which means a media file was uploaded, the alt text is required
|
|
11
|
+
if (id) {
|
|
11
12
|
if (!value || value.trim().length === 0) {
|
|
12
|
-
|
|
13
|
+
// @ts-expect-error - the translation key type does not include the custom key
|
|
14
|
+
return t('@jhb.software/payload-alt-text-plugin:theAlternateTextIsRequired');
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
|
-
// The alt text is not required when the
|
|
16
|
-
//
|
|
17
|
+
// The alt text is not required when the media file was not uploaded yet
|
|
18
|
+
// (since the alt text generation needs an URL to fetch the file)
|
|
17
19
|
return true;
|
|
18
20
|
},
|
|
19
21
|
admin: {
|
|
@@ -1 +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: '
|
|
1
|
+
{"version":3,"sources":["../../src/fields/altTextField.ts"],"sourcesContent":["import type { TextareaField } from 'payload'\nimport { translatedLabel } from '../utils/translatedLabel.js'\n\nexport function altTextField({\n localized,\n}: {\n localized?: TextareaField['localized']\n}): TextareaField {\n return {\n name: 'alt',\n label: translatedLabel('alternateText'),\n type: 'textarea',\n required: true,\n localized: localized,\n validate: (value, { id, req: { t } }) => {\n // if the document has an id, which means a media file was uploaded, the alt text is required\n if (id) {\n if (!value || value.trim().length === 0) {\n // @ts-expect-error - the translation key type does not include the custom key\n return t('@jhb.software/payload-alt-text-plugin:theAlternateTextIsRequired')\n }\n }\n\n // The alt text is not required when the media file was not uploaded yet\n // (since the alt text generation needs an URL to fetch the file)\n return true\n },\n admin: {\n components: {\n Field: '@jhb.software/payload-alt-text-plugin/client#AltTextField',\n },\n },\n }\n}\n"],"names":["translatedLabel","altTextField","localized","name","label","type","required","validate","value","id","req","t","trim","length","admin","components","Field"],"mappings":"AACA,SAASA,eAAe,QAAQ,8BAA6B;AAE7D,OAAO,SAASC,aAAa,EAC3BC,SAAS,EAGV;IACC,OAAO;QACLC,MAAM;QACNC,OAAOJ,gBAAgB;QACvBK,MAAM;QACNC,UAAU;QACVJ,WAAWA;QACXK,UAAU,CAACC,OAAO,EAAEC,EAAE,EAAEC,KAAK,EAAEC,CAAC,EAAE,EAAE;YAClC,6FAA6F;YAC7F,IAAIF,IAAI;gBACN,IAAI,CAACD,SAASA,MAAMI,IAAI,GAAGC,MAAM,KAAK,GAAG;oBACvC,8EAA8E;oBAC9E,OAAOF,EAAE;gBACX;YACF;YAEA,wEAAwE;YACxE,iEAAiE;YACjE,OAAO;QACT;QACAG,OAAO;YACLC,YAAY;gBACVC,OAAO;YACT;QACF;IACF;AACF"}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import { translatedLabel } from '../utils/translatedLabel.js';
|
|
1
2
|
export function keywordsField({ localized }) {
|
|
2
3
|
return {
|
|
3
4
|
name: 'keywords',
|
|
4
|
-
label: '
|
|
5
|
+
label: translatedLabel('keywords'),
|
|
5
6
|
type: 'text',
|
|
6
7
|
hasMany: true,
|
|
7
8
|
required: false,
|
|
8
9
|
localized: localized,
|
|
9
|
-
hidden: true,
|
|
10
10
|
admin: {
|
|
11
11
|
description: 'Keywords which describe the image. Used when searching for the image.',
|
|
12
|
-
readOnly: true
|
|
12
|
+
readOnly: true,
|
|
13
|
+
hidden: true
|
|
13
14
|
}
|
|
14
15
|
};
|
|
15
16
|
}
|
|
@@ -1 +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: '
|
|
1
|
+
{"version":3,"sources":["../../src/fields/keywordsField.ts"],"sourcesContent":["import type { TextField } from 'payload'\nimport { translatedLabel } from '../utils/translatedLabel.js'\n\nexport function keywordsField({ localized }: { localized?: TextField['localized'] }): TextField {\n return {\n name: 'keywords',\n label: translatedLabel('keywords'),\n type: 'text',\n hasMany: true,\n required: false,\n localized: localized,\n admin: {\n description: 'Keywords which describe the image. Used when searching for the image.', // does not need to be translated because it is only used for the JSDoc\n readOnly: true,\n hidden: true, // this field is only meant to be used for improving the search, therefore hide it from the UI\n },\n }\n}\n"],"names":["translatedLabel","keywordsField","localized","name","label","type","hasMany","required","admin","description","readOnly","hidden"],"mappings":"AACA,SAASA,eAAe,QAAQ,8BAA6B;AAE7D,OAAO,SAASC,cAAc,EAAEC,SAAS,EAA0C;IACjF,OAAO;QACLC,MAAM;QACNC,OAAOJ,gBAAgB;QACvBK,MAAM;QACNC,SAAS;QACTC,UAAU;QACVL,WAAWA;QACXM,OAAO;YACLC,aAAa;YACbC,UAAU;YACVC,QAAQ;QACV;IACF;AACF"}
|
package/dist/plugin.js
CHANGED
|
@@ -2,6 +2,8 @@ import { altTextField } from './fields/altTextField.js';
|
|
|
2
2
|
import { keywordsField } from './fields/keywordsField.js';
|
|
3
3
|
import { generateAltTextEndpoint } from './endpoints/generateAltText.js';
|
|
4
4
|
import { bulkGenerateAltTextsEndpoint } from './endpoints/bulkGenerateAltTexts.js';
|
|
5
|
+
import { translations } from './translations/index.js';
|
|
6
|
+
import { deepMergeSimple } from './utils/deepMergeSimple.js';
|
|
5
7
|
export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
6
8
|
const config = {
|
|
7
9
|
...incomingConfig
|
|
@@ -11,9 +13,6 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
11
13
|
return config;
|
|
12
14
|
}
|
|
13
15
|
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
16
|
const pluginConfig = {
|
|
18
17
|
enabled: incomingPluginConfig.enabled ?? true,
|
|
19
18
|
openAIApiKey: incomingPluginConfig.openAIApiKey,
|
|
@@ -21,9 +20,14 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
21
20
|
maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,
|
|
22
21
|
model: incomingPluginConfig.model ?? 'gpt-4.1-nano',
|
|
23
22
|
locales: locales,
|
|
23
|
+
locale: incomingPluginConfig.locale,
|
|
24
24
|
getImageThumbnail: incomingPluginConfig.getImageThumbnail,
|
|
25
25
|
fieldsOverride: incomingPluginConfig.fieldsOverride
|
|
26
26
|
};
|
|
27
|
+
// Validate locale requirement for non-localized mode
|
|
28
|
+
if (locales.length === 0 && !incomingPluginConfig.locale) {
|
|
29
|
+
throw new Error('The alt-text plugin requires a "locale" option when Payload localization is disabled. ' + 'Please add { locale: "en" } (or your preferred locale) to your plugin configuration.');
|
|
30
|
+
}
|
|
27
31
|
const defaultFields = [
|
|
28
32
|
altTextField({
|
|
29
33
|
localized: Boolean(config.localization)
|
|
@@ -48,11 +52,27 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
48
52
|
...collectionConfig,
|
|
49
53
|
admin: {
|
|
50
54
|
...collectionConfig.admin,
|
|
55
|
+
listSearchableFields: [
|
|
56
|
+
// enhance the search by adding the keywords and alt fields (if not already included)
|
|
57
|
+
...collectionConfig.admin?.listSearchableFields ?? [],
|
|
58
|
+
...collectionConfig.admin?.listSearchableFields?.includes('keywords') ? [] : [
|
|
59
|
+
'keywords'
|
|
60
|
+
],
|
|
61
|
+
...collectionConfig.admin?.listSearchableFields?.includes('alt') ? [] : [
|
|
62
|
+
'alt'
|
|
63
|
+
]
|
|
64
|
+
],
|
|
51
65
|
components: {
|
|
52
66
|
...collectionConfig.admin?.components ?? {},
|
|
67
|
+
// TODO: use the beforeBulkAction custom component slot once available: https://github.com/payloadcms/payload/pull/11719
|
|
53
68
|
beforeListTable: [
|
|
54
69
|
...collectionConfig.admin?.components?.beforeListTable ?? [],
|
|
55
|
-
|
|
70
|
+
{
|
|
71
|
+
path: '@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton',
|
|
72
|
+
props: {
|
|
73
|
+
collectionSlug: collectionConfig.slug
|
|
74
|
+
}
|
|
75
|
+
}
|
|
56
76
|
]
|
|
57
77
|
}
|
|
58
78
|
},
|
|
@@ -66,6 +86,10 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
66
86
|
});
|
|
67
87
|
return {
|
|
68
88
|
...config,
|
|
89
|
+
i18n: {
|
|
90
|
+
...config.i18n,
|
|
91
|
+
translations: deepMergeSimple(translations, incomingConfig.i18n?.translations ?? {})
|
|
92
|
+
},
|
|
69
93
|
custom: {
|
|
70
94
|
...config.custom,
|
|
71
95
|
// Make plugin config available in hooks/actions
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +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
|
|
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'\nimport { translations } from './translations/index.js'\nimport { deepMergeSimple } from './utils/deepMergeSimple.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 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 locale: incomingPluginConfig.locale,\n getImageThumbnail: incomingPluginConfig.getImageThumbnail,\n fieldsOverride: incomingPluginConfig.fieldsOverride,\n }\n\n // Validate locale requirement for non-localized mode\n if (locales.length === 0 && !incomingPluginConfig.locale) {\n throw new Error(\n 'The alt-text plugin requires a \"locale\" option when Payload localization is disabled. ' +\n 'Please add { locale: \"en\" } (or your preferred locale) to your plugin configuration.',\n )\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 listSearchableFields: [\n // enhance the search by adding the keywords and alt fields (if not already included)\n ...(collectionConfig.admin?.listSearchableFields ?? []),\n ...(collectionConfig.admin?.listSearchableFields?.includes('keywords')\n ? []\n : ['keywords']),\n ...(collectionConfig.admin?.listSearchableFields?.includes('alt') ? [] : ['alt']),\n ],\n components: {\n ...(collectionConfig.admin?.components ?? {}),\n // TODO: use the beforeBulkAction custom component slot once available: https://github.com/payloadcms/payload/pull/11719\n beforeListTable: [\n ...(collectionConfig.admin?.components?.beforeListTable ?? []),\n {\n path: '@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton',\n props: {\n collectionSlug: collectionConfig.slug,\n },\n },\n ],\n },\n },\n fields: [...(collectionConfig.fields ?? []), ...fields],\n }\n }\n\n return collectionConfig\n })\n\n return {\n ...config,\n i18n: {\n ...config.i18n,\n translations: deepMergeSimple(translations, incomingConfig.i18n?.translations ?? {}),\n },\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","translations","deepMergeSimple","payloadAltTextPlugin","incomingPluginConfig","incomingConfig","config","enabled","locales","localization","map","localeConfig","code","pluginConfig","openAIApiKey","collections","maxBulkGenerateConcurrency","model","locale","getImageThumbnail","fieldsOverride","length","Error","defaultFields","localized","Boolean","fields","collectionConfig","includes","slug","upload","console","warn","admin","listSearchableFields","components","beforeListTable","path","props","collectionSlug","i18n","custom","altTextPluginConfig","endpoints","method","handler"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const de = {
|
|
2
|
+
$schema: './translation-schema.json',
|
|
3
|
+
'@jhb.software/payload-alt-text-plugin': {
|
|
4
|
+
// Field labels
|
|
5
|
+
alternateText: 'Alternativtext',
|
|
6
|
+
keywords: 'Schlüsselwörter',
|
|
7
|
+
keywordsDescription: 'Schlüsselwörter, die das Bild beschreiben. Wird bei der Suche nach dem Bild verwendet.',
|
|
8
|
+
// Button labels
|
|
9
|
+
generateAltText: 'Alternativtext generieren',
|
|
10
|
+
generateAltTextFor: 'Alternativtext generieren für',
|
|
11
|
+
image: 'Bild',
|
|
12
|
+
images: 'Bilder',
|
|
13
|
+
// Toast messages
|
|
14
|
+
cannotGenerateMissingFields: 'Alternativtext kann nicht generiert werden. Erforderliche Felder fehlen.',
|
|
15
|
+
failedToGenerate: 'Generierung des Alternativtextes fehlgeschlagen. Bitte versuchen Sie es erneut.',
|
|
16
|
+
altTextGeneratedSuccess: 'Alternativtext erfolgreich generiert. Bitte überprüfen und speichern Sie das Dokument.',
|
|
17
|
+
noAltTextGenerated: 'Kein Alternativtext generiert. Bitte versuchen Sie es erneut.',
|
|
18
|
+
errorGeneratingAltText: 'Fehler beim Generieren des Alternativtextes. Bitte versuchen Sie es erneut.',
|
|
19
|
+
failedToGenerateForXImages: 'Generierung des Alternativtextes für {X} Bilder fehlgeschlagen.',
|
|
20
|
+
xOfYImagesUpdated: '{X} von {Y} Bildern aktualisiert.',
|
|
21
|
+
// Help text
|
|
22
|
+
altTextDescription: 'Alternativtext für das Bild. Dieser wird für Screenreader und SEO verwendet. Er sollte die folgenden Anforderungen erfüllen:',
|
|
23
|
+
altTextRequirement1: 'Beschreibt in 1-2 Sätzen, was auf dem Bild zu sehen ist.',
|
|
24
|
+
altTextRequirement2: 'Sollte möglichst die gleichen Informationen oder den gleichen Zweck wie das Bild vermitteln.',
|
|
25
|
+
altTextRequirement3: 'Phrasen wie "Bild von" oder "Foto von" sind überflüssig, da Screenreader bereits anzeigen, dass es sich um ein Bild handelt.',
|
|
26
|
+
// Tooltips
|
|
27
|
+
pleaseSaveDocumentFirst: 'Bitte speichern Sie zuerst das Dokument',
|
|
28
|
+
// Validation messages
|
|
29
|
+
theAlternateTextIsRequired: 'Der Alternativtext ist erforderlich.'
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=de.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/translations/de.ts"],"sourcesContent":["import { GenericTranslationsObject } from './index.js'\n\nexport const de: GenericTranslationsObject = {\n $schema: './translation-schema.json',\n '@jhb.software/payload-alt-text-plugin': {\n // Field labels\n alternateText: 'Alternativtext',\n keywords: 'Schlüsselwörter',\n keywordsDescription:\n 'Schlüsselwörter, die das Bild beschreiben. Wird bei der Suche nach dem Bild verwendet.',\n\n // Button labels\n generateAltText: 'Alternativtext generieren',\n generateAltTextFor: 'Alternativtext generieren für',\n image: 'Bild',\n images: 'Bilder',\n\n // Toast messages\n cannotGenerateMissingFields:\n 'Alternativtext kann nicht generiert werden. Erforderliche Felder fehlen.',\n failedToGenerate:\n 'Generierung des Alternativtextes fehlgeschlagen. Bitte versuchen Sie es erneut.',\n altTextGeneratedSuccess:\n 'Alternativtext erfolgreich generiert. Bitte überprüfen und speichern Sie das Dokument.',\n noAltTextGenerated: 'Kein Alternativtext generiert. Bitte versuchen Sie es erneut.',\n errorGeneratingAltText:\n 'Fehler beim Generieren des Alternativtextes. Bitte versuchen Sie es erneut.',\n failedToGenerateForXImages: 'Generierung des Alternativtextes für {X} Bilder fehlgeschlagen.',\n xOfYImagesUpdated: '{X} von {Y} Bildern aktualisiert.',\n\n // Help text\n altTextDescription:\n 'Alternativtext für das Bild. Dieser wird für Screenreader und SEO verwendet. Er sollte die folgenden Anforderungen erfüllen:',\n altTextRequirement1: 'Beschreibt in 1-2 Sätzen, was auf dem Bild zu sehen ist.',\n altTextRequirement2:\n 'Sollte möglichst die gleichen Informationen oder den gleichen Zweck wie das Bild vermitteln.',\n altTextRequirement3:\n 'Phrasen wie \"Bild von\" oder \"Foto von\" sind überflüssig, da Screenreader bereits anzeigen, dass es sich um ein Bild handelt.',\n\n // Tooltips\n pleaseSaveDocumentFirst: 'Bitte speichern Sie zuerst das Dokument',\n\n // Validation messages\n theAlternateTextIsRequired: 'Der Alternativtext ist erforderlich.',\n },\n}\n"],"names":["de","$schema","alternateText","keywords","keywordsDescription","generateAltText","generateAltTextFor","image","images","cannotGenerateMissingFields","failedToGenerate","altTextGeneratedSuccess","noAltTextGenerated","errorGeneratingAltText","failedToGenerateForXImages","xOfYImagesUpdated","altTextDescription","altTextRequirement1","altTextRequirement2","altTextRequirement3","pleaseSaveDocumentFirst","theAlternateTextIsRequired"],"mappings":"AAEA,OAAO,MAAMA,KAAgC;IAC3CC,SAAS;IACT,yCAAyC;QACvC,eAAe;QACfC,eAAe;QACfC,UAAU;QACVC,qBACE;QAEF,gBAAgB;QAChBC,iBAAiB;QACjBC,oBAAoB;QACpBC,OAAO;QACPC,QAAQ;QAER,iBAAiB;QACjBC,6BACE;QACFC,kBACE;QACFC,yBACE;QACFC,oBAAoB;QACpBC,wBACE;QACFC,4BAA4B;QAC5BC,mBAAmB;QAEnB,YAAY;QACZC,oBACE;QACFC,qBAAqB;QACrBC,qBACE;QACFC,qBACE;QAEF,WAAW;QACXC,yBAAyB;QAEzB,sBAAsB;QACtBC,4BAA4B;IAC9B;AACF,EAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const en = {
|
|
2
|
+
$schema: './translation-schema.json',
|
|
3
|
+
'@jhb.software/payload-alt-text-plugin': {
|
|
4
|
+
// Field labels
|
|
5
|
+
alternateText: 'Alternate text',
|
|
6
|
+
keywords: 'Keywords',
|
|
7
|
+
keywordsDescription: 'Keywords which describe the image. Used when searching for the image.',
|
|
8
|
+
// Button labels
|
|
9
|
+
generateAltText: 'Generate alt text',
|
|
10
|
+
generateAltTextFor: 'Generate alt text for',
|
|
11
|
+
image: 'image',
|
|
12
|
+
images: 'images',
|
|
13
|
+
// Toast messages
|
|
14
|
+
cannotGenerateMissingFields: 'Cannot generate alt text. Missing required fields.',
|
|
15
|
+
failedToGenerate: 'Failed to generate alt text. Please try again.',
|
|
16
|
+
altTextGeneratedSuccess: 'Alt text generated successfully. Please review and save the document.',
|
|
17
|
+
noAltTextGenerated: 'No alt text generated. Please try again.',
|
|
18
|
+
errorGeneratingAltText: 'Error generating alt text. Please try again.',
|
|
19
|
+
failedToGenerateForXImages: 'Failed to generate alt text for {X} images.',
|
|
20
|
+
xOfYImagesUpdated: '{X} of {Y} images updated.',
|
|
21
|
+
// Help text
|
|
22
|
+
altTextDescription: 'Alternate text for the image. This will be used for screen readers and SEO. It should meet the following requirements:',
|
|
23
|
+
altTextRequirement1: 'Describes in 1-2 sentences, what is visible in the image.',
|
|
24
|
+
altTextRequirement2: 'Should convey the same information or purpose as the image, whenever possible.',
|
|
25
|
+
altTextRequirement3: 'Phrases like "image of" or "picture of" are unnecessary, since screen readers already announce that it\'s an image.',
|
|
26
|
+
// Tooltips
|
|
27
|
+
pleaseSaveDocumentFirst: 'Please save the document first',
|
|
28
|
+
// Validation messages
|
|
29
|
+
theAlternateTextIsRequired: 'An alternate text is required.'
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=en.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/translations/en.ts"],"sourcesContent":["import { GenericTranslationsObject } from './index.js'\n\nexport const en: GenericTranslationsObject = {\n $schema: './translation-schema.json',\n '@jhb.software/payload-alt-text-plugin': {\n // Field labels\n alternateText: 'Alternate text',\n keywords: 'Keywords',\n keywordsDescription: 'Keywords which describe the image. Used when searching for the image.',\n\n // Button labels\n generateAltText: 'Generate alt text',\n generateAltTextFor: 'Generate alt text for',\n image: 'image',\n images: 'images',\n\n // Toast messages\n cannotGenerateMissingFields: 'Cannot generate alt text. Missing required fields.',\n failedToGenerate: 'Failed to generate alt text. Please try again.',\n altTextGeneratedSuccess:\n 'Alt text generated successfully. Please review and save the document.',\n noAltTextGenerated: 'No alt text generated. Please try again.',\n errorGeneratingAltText: 'Error generating alt text. Please try again.',\n failedToGenerateForXImages: 'Failed to generate alt text for {X} images.',\n xOfYImagesUpdated: '{X} of {Y} images updated.',\n\n // Help text\n altTextDescription:\n 'Alternate text for the image. This will be used for screen readers and SEO. It should meet the following requirements:',\n altTextRequirement1: 'Describes in 1-2 sentences, what is visible in the image.',\n altTextRequirement2:\n 'Should convey the same information or purpose as the image, whenever possible.',\n altTextRequirement3:\n 'Phrases like \"image of\" or \"picture of\" are unnecessary, since screen readers already announce that it\\'s an image.',\n\n // Tooltips\n pleaseSaveDocumentFirst: 'Please save the document first',\n\n // Validation messages\n theAlternateTextIsRequired: 'An alternate text is required.',\n },\n}\n"],"names":["en","$schema","alternateText","keywords","keywordsDescription","generateAltText","generateAltTextFor","image","images","cannotGenerateMissingFields","failedToGenerate","altTextGeneratedSuccess","noAltTextGenerated","errorGeneratingAltText","failedToGenerateForXImages","xOfYImagesUpdated","altTextDescription","altTextRequirement1","altTextRequirement2","altTextRequirement3","pleaseSaveDocumentFirst","theAlternateTextIsRequired"],"mappings":"AAEA,OAAO,MAAMA,KAAgC;IAC3CC,SAAS;IACT,yCAAyC;QACvC,eAAe;QACfC,eAAe;QACfC,UAAU;QACVC,qBAAqB;QAErB,gBAAgB;QAChBC,iBAAiB;QACjBC,oBAAoB;QACpBC,OAAO;QACPC,QAAQ;QAER,iBAAiB;QACjBC,6BAA6B;QAC7BC,kBAAkB;QAClBC,yBACE;QACFC,oBAAoB;QACpBC,wBAAwB;QACxBC,4BAA4B;QAC5BC,mBAAmB;QAEnB,YAAY;QACZC,oBACE;QACFC,qBAAqB;QACrBC,qBACE;QACFC,qBACE;QAEF,WAAW;QACXC,yBAAyB;QAEzB,sBAAsB;QACtBC,4BAA4B;IAC9B;AACF,EAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type GenericTranslationsObject = {
|
|
2
|
+
[key: string]: GenericTranslationsObject | string;
|
|
3
|
+
};
|
|
4
|
+
export type NestedKeysStripped<T> = T extends object ? {
|
|
5
|
+
[K in keyof T]-?: K extends string ? T[K] extends object ? `${K}:${NestedKeysStripped<T[K]>}` : `${StripCountVariants<K>}` : never;
|
|
6
|
+
}[keyof T] : '';
|
|
7
|
+
export type StripCountVariants<TKey> = TKey extends `${infer Base}_many` | `${infer Base}_one` | `${infer Base}_other` ? Base : TKey;
|
|
8
|
+
export declare const translations: {
|
|
9
|
+
de: GenericTranslationsObject;
|
|
10
|
+
en: GenericTranslationsObject;
|
|
11
|
+
};
|
|
12
|
+
export type PluginAltTextTranslations = GenericTranslationsObject;
|
|
13
|
+
export type PluginAltTextTranslationKeys = NestedKeysStripped<PluginAltTextTranslations>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import { de } from './de.js'\nimport { en } from './en.js'\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type GenericTranslationsObject = {\n [key: string]: GenericTranslationsObject | string\n}\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type NestedKeysStripped<T> = T extends object\n ? {\n [K in keyof T]-?: K extends string\n ? T[K] extends object\n ? `${K}:${NestedKeysStripped<T[K]>}`\n : `${StripCountVariants<K>}`\n : never\n }[keyof T]\n : ''\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type StripCountVariants<TKey> = TKey extends\n | `${infer Base}_many`\n | `${infer Base}_one`\n | `${infer Base}_other`\n ? Base\n : TKey\n\nexport const translations = {\n de,\n en,\n}\n\nexport type PluginAltTextTranslations = GenericTranslationsObject\n\nexport type PluginAltTextTranslationKeys = NestedKeysStripped<PluginAltTextTranslations>\n"],"names":["de","en","translations"],"mappings":"AAAA,SAASA,EAAE,QAAQ,UAAS;AAC5B,SAASC,EAAE,QAAQ,UAAS;AA0B5B,OAAO,MAAMC,eAAe;IAC1BF;IACAC;AACF,EAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"@jhb.software/payload-alt-text-plugin": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"alternateText": { "type": "string" },
|
|
9
|
+
"keywords": { "type": "string" },
|
|
10
|
+
"keywordsDescription": { "type": "string" },
|
|
11
|
+
"generateAltText": { "type": "string" },
|
|
12
|
+
"generateAltTextFor": { "type": "string" },
|
|
13
|
+
"image": { "type": "string" },
|
|
14
|
+
"images": { "type": "string" },
|
|
15
|
+
"cannotGenerateMissingFields": { "type": "string" },
|
|
16
|
+
"failedToGenerate": { "type": "string" },
|
|
17
|
+
"altTextGeneratedSuccess": { "type": "string" },
|
|
18
|
+
"noAltTextGenerated": { "type": "string" },
|
|
19
|
+
"errorGeneratingAltText": { "type": "string" },
|
|
20
|
+
"failedToGenerateForXImages": { "type": "string" },
|
|
21
|
+
"xOfYImagesUpdated": { "type": "string" },
|
|
22
|
+
"altTextDescription": { "type": "string" },
|
|
23
|
+
"altTextRequirement1": { "type": "string" },
|
|
24
|
+
"altTextRequirement2": { "type": "string" },
|
|
25
|
+
"altTextRequirement3": { "type": "string" },
|
|
26
|
+
"pleaseSaveDocumentFirst": { "type": "string" },
|
|
27
|
+
"theAlternateTextIsRequired": { "type": "string" }
|
|
28
|
+
},
|
|
29
|
+
"required": [
|
|
30
|
+
"alternateText",
|
|
31
|
+
"keywords",
|
|
32
|
+
"keywordsDescription",
|
|
33
|
+
"generateAltText",
|
|
34
|
+
"generateAltTextFor",
|
|
35
|
+
"image",
|
|
36
|
+
"images",
|
|
37
|
+
"cannotGenerateMissingFields",
|
|
38
|
+
"failedToGenerate",
|
|
39
|
+
"altTextGeneratedSuccess",
|
|
40
|
+
"noAltTextGenerated",
|
|
41
|
+
"errorGeneratingAltText",
|
|
42
|
+
"failedToGenerateForXImages",
|
|
43
|
+
"xOfYImagesUpdated",
|
|
44
|
+
"altTextDescription",
|
|
45
|
+
"altTextRequirement1",
|
|
46
|
+
"altTextRequirement2",
|
|
47
|
+
"altTextRequirement3",
|
|
48
|
+
"pleaseSaveDocumentFirst",
|
|
49
|
+
"theAlternateTextIsRequired"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"required": ["@jhb.software/payload-alt-text-plugin"]
|
|
54
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Field } from 'payload';
|
|
1
|
+
import { CollectionSlug, Field } from 'payload';
|
|
2
2
|
/** Configuration options for the alt text plugin. */
|
|
3
3
|
export type IncomingAltTextPluginConfig = {
|
|
4
4
|
/** Whether the plugin is enabled. */
|
|
@@ -6,7 +6,7 @@ export type IncomingAltTextPluginConfig = {
|
|
|
6
6
|
/** OpenAI API key for authentication. */
|
|
7
7
|
openAIApiKey: string;
|
|
8
8
|
/** Collection slugs to enable the plugin for. */
|
|
9
|
-
collections:
|
|
9
|
+
collections: CollectionSlug[];
|
|
10
10
|
/** Maximum number of concurrent API requests for bulk operations. */
|
|
11
11
|
maxBulkGenerateConcurrency?: number;
|
|
12
12
|
/**
|
|
@@ -24,6 +24,12 @@ export type IncomingAltTextPluginConfig = {
|
|
|
24
24
|
fieldsOverride?: (args: {
|
|
25
25
|
defaultFields: Field[];
|
|
26
26
|
}) => Field[];
|
|
27
|
+
/**
|
|
28
|
+
* The locale to generate alt texts in when localization is disabled.
|
|
29
|
+
* Required when localization is disabled, ignored when localization is enabled.
|
|
30
|
+
* @example 'en', 'de'
|
|
31
|
+
*/
|
|
32
|
+
locale?: string;
|
|
27
33
|
};
|
|
28
34
|
/** Configuration of the alt text plugin after defaults have been applied. */
|
|
29
35
|
export type AltTextPluginConfig = {
|
|
@@ -32,7 +38,7 @@ export type AltTextPluginConfig = {
|
|
|
32
38
|
/** OpenAI API key for authentication. */
|
|
33
39
|
openAIApiKey: string;
|
|
34
40
|
/** Collection slugs to enable the plugin for. */
|
|
35
|
-
collections:
|
|
41
|
+
collections: CollectionSlug[];
|
|
36
42
|
/** Maximum number of concurrent API requests for bulk generate operations. */
|
|
37
43
|
maxBulkGenerateConcurrency: number;
|
|
38
44
|
/** Function to get the thumbnail URL of an image document. */
|
|
@@ -45,4 +51,6 @@ export type AltTextPluginConfig = {
|
|
|
45
51
|
}) => Field[];
|
|
46
52
|
/** The locales to generate alt texts for. */
|
|
47
53
|
locales: string[];
|
|
54
|
+
/** The locale to generate alt texts in when localization is disabled. */
|
|
55
|
+
locale?: string;
|
|
48
56
|
};
|
|
@@ -1 +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:
|
|
1
|
+
{"version":3,"sources":["../../src/types/AltTextPluginConfig.ts"],"sourcesContent":["import { CollectionSlug, 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: CollectionSlug[]\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 * The locale to generate alt texts in when localization is disabled.\n * Required when localization is disabled, ignored when localization is enabled.\n * @example 'en', 'de'\n */\n locale?: string\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: CollectionSlug[]\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 /** The locale to generate alt texts in when localization is disabled. */\n locale?: string\n}\n"],"names":[],"mappings":"AAwCA,2EAA2E,GAC3E,WA2BC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Returns the locales from the config. Returns undefined when localization is disabled. */ export function localesFromConfig(config) {
|
|
2
|
+
if (typeof config.localization === 'object' && config.localization) {
|
|
3
|
+
return config.localization.localeCodes;
|
|
4
|
+
} else {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=localesFromConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/localesFromConfig.ts"],"sourcesContent":["import { SanitizedConfig } from 'payload'\n\n/** Returns the locales from the config. Returns undefined when localization is disabled. */\nexport function localesFromConfig(config: SanitizedConfig): string[] | undefined {\n if (typeof config.localization === 'object' && config.localization) {\n return config.localization.localeCodes\n } else {\n return undefined\n }\n}\n"],"names":["localesFromConfig","config","localization","localeCodes","undefined"],"mappings":"AAEA,0FAA0F,GAC1F,OAAO,SAASA,kBAAkBC,MAAuB;IACvD,IAAI,OAAOA,OAAOC,YAAY,KAAK,YAAYD,OAAOC,YAAY,EAAE;QAClE,OAAOD,OAAOC,YAAY,CAACC,WAAW;IACxC,OAAO;QACL,OAAOC;IACT;AACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Very simple, but fast deepMerge implementation. Only deepMerges objects, not arrays and clones everything.
|
|
3
|
+
* Do not use this if your object contains any complex objects like React Components, or if you would like to combine Arrays.
|
|
4
|
+
* If you only have simple objects and need a fast deepMerge, this is the function for you.
|
|
5
|
+
*
|
|
6
|
+
* obj2 takes precedence over obj1 - thus if obj2 has a key that obj1 also has, obj2's value will be used.
|
|
7
|
+
*
|
|
8
|
+
* @param obj1 base object
|
|
9
|
+
* @param obj2 object to merge "into" obj1
|
|
10
|
+
*/
|
|
11
|
+
export declare function deepMergeSimple<T = object>(obj1: object, obj2: object): T;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/utilities/deepMergeSimple.ts
|
|
2
|
+
/**
|
|
3
|
+
* Very simple, but fast deepMerge implementation. Only deepMerges objects, not arrays and clones everything.
|
|
4
|
+
* Do not use this if your object contains any complex objects like React Components, or if you would like to combine Arrays.
|
|
5
|
+
* If you only have simple objects and need a fast deepMerge, this is the function for you.
|
|
6
|
+
*
|
|
7
|
+
* obj2 takes precedence over obj1 - thus if obj2 has a key that obj1 also has, obj2's value will be used.
|
|
8
|
+
*
|
|
9
|
+
* @param obj1 base object
|
|
10
|
+
* @param obj2 object to merge "into" obj1
|
|
11
|
+
*/ export function deepMergeSimple(obj1, obj2) {
|
|
12
|
+
const output = {
|
|
13
|
+
...obj1
|
|
14
|
+
};
|
|
15
|
+
for(const key in obj2){
|
|
16
|
+
if (Object.prototype.hasOwnProperty.call(obj2, key)) {
|
|
17
|
+
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
|
18
|
+
if (typeof obj2[key] === 'object' && !Array.isArray(obj2[key]) && obj1[key]) {
|
|
19
|
+
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
|
20
|
+
output[key] = deepMergeSimple(obj1[key], obj2[key]);
|
|
21
|
+
} else {
|
|
22
|
+
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
|
23
|
+
output[key] = obj2[key];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return output;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=deepMergeSimple.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/deepMergeSimple.ts"],"sourcesContent":["// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/utilities/deepMergeSimple.ts\n\n/**\n * Very simple, but fast deepMerge implementation. Only deepMerges objects, not arrays and clones everything.\n * Do not use this if your object contains any complex objects like React Components, or if you would like to combine Arrays.\n * If you only have simple objects and need a fast deepMerge, this is the function for you.\n *\n * obj2 takes precedence over obj1 - thus if obj2 has a key that obj1 also has, obj2's value will be used.\n *\n * @param obj1 base object\n * @param obj2 object to merge \"into\" obj1\n */\nexport function deepMergeSimple<T = object>(obj1: object, obj2: object): T {\n const output = { ...obj1 }\n\n for (const key in obj2) {\n if (Object.prototype.hasOwnProperty.call(obj2, key)) {\n // @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve\n if (typeof obj2[key] === 'object' && !Array.isArray(obj2[key]) && obj1[key]) {\n // @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve\n output[key] = deepMergeSimple(obj1[key], obj2[key])\n } else {\n // @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve\n output[key] = obj2[key]\n }\n }\n }\n\n return output as T\n}\n"],"names":["deepMergeSimple","obj1","obj2","output","key","Object","prototype","hasOwnProperty","call","Array","isArray"],"mappings":"AAAA,qHAAqH;AAErH;;;;;;;;;CASC,GACD,OAAO,SAASA,gBAA4BC,IAAY,EAAEC,IAAY;IACpE,MAAMC,SAAS;QAAE,GAAGF,IAAI;IAAC;IAEzB,IAAK,MAAMG,OAAOF,KAAM;QACtB,IAAIG,OAAOC,SAAS,CAACC,cAAc,CAACC,IAAI,CAACN,MAAME,MAAM;YACnD,oFAAoF;YACpF,IAAI,OAAOF,IAAI,CAACE,IAAI,KAAK,YAAY,CAACK,MAAMC,OAAO,CAACR,IAAI,CAACE,IAAI,KAAKH,IAAI,CAACG,IAAI,EAAE;gBAC3E,oFAAoF;gBACpFD,MAAM,CAACC,IAAI,GAAGJ,gBAAgBC,IAAI,CAACG,IAAI,EAAEF,IAAI,CAACE,IAAI;YACpD,OAAO;gBACL,oFAAoF;gBACpFD,MAAM,CAACC,IAAI,GAAGF,IAAI,CAACE,IAAI;YACzB;QACF;IACF;IAEA,OAAOD;AACT"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { translations } from '../translations/index.js';
|
|
2
|
+
/** Returns the StaticLabel object for the given translation to to use inside the field label. */ export function translatedLabel(key) {
|
|
3
|
+
return Object.fromEntries(Object.entries(translations).map(([locale, translation])=>[
|
|
4
|
+
locale,
|
|
5
|
+
translation['@jhb.software/payload-alt-text-plugin'][key] || key
|
|
6
|
+
]));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=translatedLabel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/translatedLabel.ts"],"sourcesContent":["import { StaticLabel } from 'payload'\nimport { translations } from '../translations/index.js'\n\n/** Returns the StaticLabel object for the given translation to to use inside the field label. */\nexport function translatedLabel(key: string): StaticLabel {\n return Object.fromEntries(\n Object.entries(translations).map(([locale, translation]) => [\n locale,\n (translation['@jhb.software/payload-alt-text-plugin'] as Record<string, string>)[key] || key,\n ]),\n )\n}\n"],"names":["translations","translatedLabel","key","Object","fromEntries","entries","map","locale","translation"],"mappings":"AACA,SAASA,YAAY,QAAQ,2BAA0B;AAEvD,+FAA+F,GAC/F,OAAO,SAASC,gBAAgBC,GAAW;IACzC,OAAOC,OAAOC,WAAW,CACvBD,OAAOE,OAAO,CAACL,cAAcM,GAAG,CAAC,CAAC,CAACC,QAAQC,YAAY,GAAK;YAC1DD;YACCC,WAAW,CAAC,wCAAwC,AAA2B,CAACN,IAAI,IAAIA;SAC1F;AAEL"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useTranslation } from '@payloadcms/ui';
|
|
2
|
+
/** Hook which returns a translation function for the plugin translations. */ export const usePluginTranslation = ()=>{
|
|
3
|
+
const { i18n } = useTranslation();
|
|
4
|
+
const pluginTranslations = i18n.translations['@jhb.software/payload-alt-text-plugin'];
|
|
5
|
+
return {
|
|
6
|
+
t: (key)=>{
|
|
7
|
+
const translation = pluginTranslations[key];
|
|
8
|
+
if (!translation) {
|
|
9
|
+
console.log('Plugin translation not found', key);
|
|
10
|
+
}
|
|
11
|
+
return translation ?? key;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=usePluginTranslation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/usePluginTranslation.ts"],"sourcesContent":["import { useTranslation } from '@payloadcms/ui'\nimport { PluginAltTextTranslationKeys } from 'src/translations/index.js'\nimport { PluginAltTextTranslations } from 'src/translations/index.js'\n\n/** Hook which returns a translation function for the plugin translations. */\nexport const usePluginTranslation = () => {\n const { i18n } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const pluginTranslations = i18n.translations[\n '@jhb.software/payload-alt-text-plugin'\n ] as PluginAltTextTranslations\n\n return {\n t: (key: PluginAltTextTranslationKeys) => {\n const translation = pluginTranslations[key] as string\n\n if (!translation) {\n console.log('Plugin translation not found', key)\n }\n return translation ?? key\n },\n }\n}\n"],"names":["useTranslation","usePluginTranslation","i18n","pluginTranslations","translations","t","key","translation","console","log"],"mappings":"AAAA,SAASA,cAAc,QAAQ,iBAAgB;AAI/C,2EAA2E,GAC3E,OAAO,MAAMC,uBAAuB;IAClC,MAAM,EAAEC,IAAI,EAAE,GAAGF;IACjB,MAAMG,qBAAqBD,KAAKE,YAAY,CAC1C,wCACD;IAED,OAAO;QACLC,GAAG,CAACC;YACF,MAAMC,cAAcJ,kBAAkB,CAACG,IAAI;YAE3C,IAAI,CAACC,aAAa;gBAChBC,QAAQC,GAAG,CAAC,gCAAgCH;YAC9C;YACA,OAAOC,eAAeD;QACxB;IACF;AACF,EAAC"}
|
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.2.1",
|
|
4
4
|
"description": "A minimal Payload CMS plugin to generate image alt texts using OpenAI's Vision API.",
|
|
5
5
|
"bugs": "https://github.com/jhb-software/payload-plugins/issues",
|
|
6
6
|
"repository": "https://github.com/jhb-software/payload-plugins",
|