@jhb.software/payload-alt-text-plugin 0.2.2 → 0.3.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.
Files changed (56) hide show
  1. package/README.md +93 -22
  2. package/dist/components/AltTextField.js +6 -6
  3. package/dist/components/AltTextField.js.map +1 -1
  4. package/dist/components/BulkGenerateAltTextsButton.js +15 -16
  5. package/dist/components/BulkGenerateAltTextsButton.js.map +1 -1
  6. package/dist/components/GenerateAltTextButton.js +24 -25
  7. package/dist/components/GenerateAltTextButton.js.map +1 -1
  8. package/dist/components/icons/Lightning.js +4 -4
  9. package/dist/components/icons/Lightning.js.map +1 -1
  10. package/dist/components/icons/Spinner.js +11 -11
  11. package/dist/components/icons/Spinner.js.map +1 -1
  12. package/dist/endpoints/bulkGenerateAltTexts.js +30 -77
  13. package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
  14. package/dist/endpoints/generateAltText.d.ts +1 -1
  15. package/dist/endpoints/generateAltText.js +19 -74
  16. package/dist/endpoints/generateAltText.js.map +1 -1
  17. package/dist/exports/client.d.ts +1 -1
  18. package/dist/exports/client.js +1 -1
  19. package/dist/exports/client.js.map +1 -1
  20. package/dist/fields/altTextField.js +20 -15
  21. package/dist/fields/altTextField.js.map +1 -1
  22. package/dist/fields/keywordsField.js +7 -7
  23. package/dist/fields/keywordsField.js.map +1 -1
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/plugin.js +24 -32
  28. package/dist/plugin.js.map +1 -1
  29. package/dist/resolvers/openAI.d.ts +24 -0
  30. package/dist/resolvers/openAI.js +177 -0
  31. package/dist/resolvers/openAI.js.map +1 -0
  32. package/dist/resolvers/types.d.ts +68 -0
  33. package/dist/resolvers/types.js +6 -0
  34. package/dist/resolvers/types.js.map +1 -0
  35. package/dist/translations/de.d.ts +1 -1
  36. package/dist/translations/de.js +3 -3
  37. package/dist/translations/de.js.map +1 -1
  38. package/dist/translations/en.d.ts +1 -1
  39. package/dist/translations/en.js +3 -3
  40. package/dist/translations/en.js.map +1 -1
  41. package/dist/types/AltTextPluginConfig.d.ts +28 -26
  42. package/dist/types/AltTextPluginConfig.js.map +1 -1
  43. package/dist/utilities/localesFromConfig.d.ts +1 -1
  44. package/dist/utilities/localesFromConfig.js.map +1 -1
  45. package/dist/utils/translatedLabel.d.ts +1 -1
  46. package/dist/utils/translatedLabel.js.map +1 -1
  47. package/package.json +20 -18
  48. package/dist/utilities/getGenerationCost.d.ts +0 -12
  49. package/dist/utilities/getGenerationCost.js +0 -30
  50. package/dist/utilities/getGenerationCost.js.map +0 -1
  51. package/dist/utilities/zodResponseFormat.d.ts +0 -11
  52. package/dist/utilities/zodResponseFormat.js +0 -23
  53. package/dist/utilities/zodResponseFormat.js.map +0 -1
  54. package/dist/utils/usePluginTranslation.d.ts +0 -5
  55. package/dist/utils/usePluginTranslation.js +0 -16
  56. package/dist/utils/usePluginTranslation.js.map +0 -1
@@ -1,8 +1,5 @@
1
- import OpenAI from 'openai';
2
1
  import pMap from 'p-map';
3
2
  import { z } from 'zod';
4
- import { getGenerationCost } from '../utilities/getGenerationCost.js';
5
- import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
6
3
  import { localesFromConfig } from '../utilities/localesFromConfig.js';
7
4
  /**
8
5
  * Generates and updates alt text for multiple images in all locales.
@@ -32,15 +29,14 @@ import { localesFromConfig } from '../utilities/localesFromConfig.js';
32
29
  status: 500
33
30
  });
34
31
  }
35
- if (!pluginConfig.openAIApiKey) {
32
+ if (!pluginConfig.resolver) {
36
33
  return Response.json({
37
- error: 'OpenAI API key not configured'
34
+ error: 'No alt text resolver configured'
38
35
  }, {
39
36
  status: 500
40
37
  });
41
38
  }
42
- // Use concurrency from config
43
- const concurrency = pluginConfig.maxBulkGenerateConcurrency || 16;
39
+ const concurrency = pluginConfig.maxBulkGenerateConcurrency;
44
40
  // determine target locales based on config
45
41
  const locales = localesFromConfig(req.payload.config);
46
42
  const targetLocales = locales ?? [
@@ -56,11 +52,12 @@ import { localesFromConfig } from '../utilities/localesFromConfig.js';
56
52
  await pMap(ids, async (id)=>{
57
53
  try {
58
54
  await generateAndUpdateAltText({
59
- payload: req.payload,
60
55
  id,
61
56
  collection,
57
+ locales: targetLocales,
58
+ payload: req.payload,
62
59
  pluginConfig,
63
- locales: targetLocales
60
+ req
64
61
  });
65
62
  updatedDocs++;
66
63
  console.log(`${updatedDocs}/${ids.length} updated (${Math.round(updatedDocs / ids.length * 100)}%)`);
@@ -75,9 +72,9 @@ import { localesFromConfig } from '../utilities/localesFromConfig.js';
75
72
  console.error(`Failed for: ${erroredDocs.join(', ')}`);
76
73
  }
77
74
  return Response.json({
78
- updatedDocs,
75
+ erroredDocs,
79
76
  totalDocs: ids.length,
80
- erroredDocs
77
+ updatedDocs
81
78
  });
82
79
  } catch (error) {
83
80
  console.error('Error in bulk generation:', error);
@@ -88,82 +85,38 @@ import { localesFromConfig } from '../utilities/localesFromConfig.js';
88
85
  });
89
86
  }
90
87
  };
91
- async function generateAndUpdateAltText({ payload, id, collection, pluginConfig, locales }) {
88
+ async function generateAndUpdateAltText({ id, collection, locales, payload, pluginConfig, req }) {
92
89
  const imageDoc = await payload.findByID({
93
- collection: collection,
94
- id: id,
90
+ id,
91
+ collection,
95
92
  depth: 0
96
93
  });
97
94
  if (!imageDoc) {
98
95
  throw new Error('Image not found');
99
96
  }
100
97
  const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc);
101
- const openai = new OpenAI({
102
- apiKey: pluginConfig.openAIApiKey
103
- });
104
- const modelResponseSchema = z.object(Object.fromEntries(locales.map((locale)=>[
105
- locale,
106
- z.object({
107
- altText: z.string().describe('A concise, descriptive alt text for the image'),
108
- keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
109
- })
110
- ])));
111
- const response = await openai.chat.completions.parse({
112
- model: pluginConfig.model,
113
- messages: [
114
- {
115
- role: 'system',
116
- content: `
117
- You are an expert at analyzing images and creating descriptive image alt text.
118
-
119
- Please analyze the given image and provide the following in ${locales.join(', ')}:
120
- - 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.
121
- - A localized list of keywords that describe the content (e.g., ["Camel", "Palm trees", "Desert"]) as "keywords"
122
-
123
- If a context is provided, use it to enhance the alt text.
124
-
125
- Format your response as a JSON object with ${locales.join(', ')} keys, each containing "altText", "keywords" and "slug".
126
- `
127
- },
128
- {
129
- role: 'user',
130
- content: [
131
- {
132
- type: 'image_url',
133
- image_url: {
134
- url: imageThumbnailUrl
135
- }
136
- },
137
- ...'filename' in imageDoc && imageDoc.filename ? [
138
- {
139
- type: 'text',
140
- text: imageDoc.filename
141
- }
142
- ] : []
143
- ]
144
- }
145
- ],
146
- max_completion_tokens: 300,
147
- response_format: zodResponseFormat(modelResponseSchema, 'data')
148
- });
149
- console.log({
150
- imageId: id,
151
- ...getGenerationCost(response, pluginConfig.model)
98
+ const result = await pluginConfig.resolver.resolveBulk({
99
+ filename: 'filename' in imageDoc && typeof imageDoc.filename === 'string' ? imageDoc.filename : undefined,
100
+ imageThumbnailUrl,
101
+ locales,
102
+ req
152
103
  });
153
- const result = response.choices[0]?.message?.parsed;
154
- if (!result) {
155
- throw new Error('No result from OpenAI');
104
+ if (!result.success) {
105
+ throw new Error(result.error || 'Failed to generate alt text');
156
106
  }
157
107
  for (const locale of locales){
158
- await payload.update({
159
- collection: collection,
160
- id: id,
161
- locale: locale,
162
- data: {
163
- alt: result[locale]?.altText,
164
- keywords: result[locale]?.keywords
165
- }
166
- });
108
+ const localeResult = result.results[locale];
109
+ if (localeResult) {
110
+ await payload.update({
111
+ id,
112
+ collection,
113
+ data: {
114
+ alt: localeResult.altText,
115
+ keywords: localeResult.keywords
116
+ },
117
+ locale
118
+ });
119
+ }
167
120
  }
168
121
  }
169
122
 
@@ -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'\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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"}
1
+ {"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { z } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\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.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // determine target locales based on config\n const locales = localesFromConfig(req.payload.config)\n const targetLocales = locales ?? [pluginConfig.locale!]\n if (!targetLocales) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n ids,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n updatedDocs++\n console.log(\n `${updatedDocs}/${ids.length} updated (${Math.round((updatedDocs / ids.length) * 100)}%)`,\n )\n } catch (error) {\n console.error(`Error generating alt text for ${id}:`, error)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n console.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n totalDocs: ids.length,\n updatedDocs,\n })\n } catch (error) {\n console.error('Error in bulk generation:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n}\n\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}) {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n })\n }\n }\n}\n"],"names":["pMap","z","localesFromConfig","bulkGenerateAltTextsEndpoint","req","user","Response","json","error","status","data","schema","object","collection","string","ids","array","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","resolver","concurrency","maxBulkGenerateConcurrency","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","length","Math","round","push","join","totalDocs","Error","message","imageDoc","findByID","depth","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","undefined","success","localeResult","results","update","alt","altText","keywords"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import type { PayloadHandler } from 'payload';
2
2
  /**
3
- * Generates alt text for a single image using OpenAI Vision API.
3
+ * Generates alt text for a single image using the configured resolver.
4
4
  * Returns result without updating the document.
5
5
  */
6
6
  export declare const generateAltTextEndpoint: PayloadHandler;
@@ -1,9 +1,6 @@
1
- import OpenAI from 'openai';
2
1
  import { z } from 'zod';
3
- import { getGenerationCost } from '../utilities/getGenerationCost.js';
4
- import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
5
2
  /**
6
- * Generates alt text for a single image using OpenAI Vision API.
3
+ * Generates alt text for a single image using the configured resolver.
7
4
  * Returns result without updating the document.
8
5
  */ export const generateAltTextEndpoint = async (req)=>{
9
6
  try {
@@ -16,14 +13,14 @@ import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
16
13
  }
17
14
  const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null;
18
15
  const requestSchema = z.object({
19
- collection: z.string(),
20
16
  id: z.string(),
17
+ collection: z.string(),
21
18
  locale: z.string().nullable()
22
19
  });
23
- const { collection, id, locale } = requestSchema.parse(data);
20
+ const { id, collection, locale } = requestSchema.parse(data);
24
21
  const imageDoc = await req.payload.findByID({
25
- collection,
26
22
  id,
23
+ collection,
27
24
  depth: 0
28
25
  });
29
26
  if (!imageDoc) {
@@ -48,89 +45,37 @@ import { zodResponseFormat } from '../utilities/zodResponseFormat.js';
48
45
  status: 500
49
46
  });
50
47
  }
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
- }
60
- const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc);
61
- if (!imageThumbnailUrl) {
48
+ if (!pluginConfig.resolver) {
62
49
  return Response.json({
63
- error: 'Image thumbnail URL not defined'
50
+ error: 'No alt text resolver configured'
64
51
  }, {
65
52
  status: 500
66
53
  });
67
54
  }
68
- if (!imageThumbnailUrl.startsWith('https://') && !imageThumbnailUrl.includes('http://')) {
55
+ // determine target locale
56
+ const targetLocale = locale ?? pluginConfig.locale;
57
+ if (!targetLocale) {
69
58
  return Response.json({
70
- error: 'Image thumbnail URL is not a valid URL. It must start with https:// or http://'
59
+ error: 'Could not determine target locale for alt text generation. Please check your plugin configuration.'
71
60
  }, {
72
61
  status: 500
73
62
  });
74
63
  }
75
- const openai = new OpenAI({
76
- apiKey: pluginConfig.openAIApiKey
77
- });
78
- const modelResponseSchema = z.object({
79
- altText: z.string().describe('A concise, descriptive alt text for the image'),
80
- keywords: z.array(z.string()).describe('Keywords that describe the content of the image')
81
- });
82
- const response = await openai.chat.completions.parse({
83
- model: pluginConfig.model,
84
- messages: [
85
- {
86
- role: 'system',
87
- content: `
88
- You are an expert at analyzing images and creating descriptive image alt text.
89
-
90
- Please analyze the given image and provide the following:
91
- - 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.
92
- - A list of keywords that describe the content (e.g., ["Camel", "Palm trees", "Desert"]) as "keywords"
93
-
94
- If a context is provided, use it to enhance the alt text.
95
-
96
- Format your response as a JSON object. You must respond in the ${targetLocale} language.
97
- `
98
- },
99
- {
100
- role: 'user',
101
- content: [
102
- {
103
- type: 'image_url',
104
- image_url: {
105
- url: imageThumbnailUrl
106
- }
107
- },
108
- ...'filename' in imageDoc && imageDoc.filename ? [
109
- {
110
- type: 'text',
111
- text: imageDoc.filename
112
- }
113
- ] : []
114
- ]
115
- }
116
- ],
117
- // limit the response tokens and costs per request
118
- max_completion_tokens: 150,
119
- response_format: zodResponseFormat(modelResponseSchema, 'data')
120
- });
121
- console.log({
122
- imageId: id,
123
- ...getGenerationCost(response, pluginConfig.model)
64
+ const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc);
65
+ const result = await pluginConfig.resolver.resolve({
66
+ filename: 'filename' in imageDoc && typeof imageDoc.filename === 'string' ? imageDoc.filename : undefined,
67
+ imageThumbnailUrl,
68
+ locale: targetLocale,
69
+ req
124
70
  });
125
- const result = response.choices[0]?.message?.parsed;
126
- if (!result) {
71
+ if (!result.success) {
127
72
  return Response.json({
128
- error: 'No result from OpenAI'
73
+ error: result.error || 'Failed to generate alt text'
129
74
  }, {
130
75
  status: 500
131
76
  });
132
77
  }
133
- return Response.json(result);
78
+ return Response.json(result.result);
134
79
  } catch (error) {
135
80
  console.error('Error generating alt text:', error);
136
81
  return Response.json({
@@ -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().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
+ {"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport { z } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\n/**\n * Generates alt text for a single image using the configured resolver.\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 id: z.string(),\n collection: z.string(),\n locale: z.string().nullable(),\n })\n\n const { id, collection, locale } = requestSchema.parse(data)\n\n const imageDoc = await req.payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n return Response.json({ error: 'Image not found' }, { status: 404 })\n }\n\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n if (!pluginConfig.getImageThumbnail) {\n return Response.json({ error: 'getImageThumbnail function not configured' }, { status: 500 })\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver 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 const result = await pluginConfig.resolver.resolve({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locale: targetLocale,\n req,\n })\n\n if (!result.success) {\n return Response.json(\n { error: result.error || 'Failed to generate alt text' },\n { status: 500 },\n )\n }\n\n return Response.json(result.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":["z","generateAltTextEndpoint","req","user","Response","json","error","status","data","requestSchema","object","id","string","collection","locale","nullable","parse","imageDoc","payload","findByID","depth","pluginConfig","config","custom","altTextPluginConfig","getImageThumbnail","resolver","targetLocale","imageThumbnailUrl","result","resolve","filename","undefined","success","console","Error","message"],"mappings":"AAEA,SAASA,CAAC,QAAQ,MAAK;AAIvB;;;CAGC,GACD,OAAO,MAAMC,0BAA0C,OAAOC;IAC5D,IAAI;QACF,IAAI,CAACA,IAAIC,IAAI,EAAE;YACb,OAAOC,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAe,GAAG;gBAAEC,QAAQ;YAAI;QAChE;QAEA,MAAMC,OAAO,UAAUN,OAAO,OAAOA,IAAIG,IAAI,KAAK,aAAa,MAAMH,IAAIG,IAAI,KAAK;QAElF,MAAMI,gBAAgBT,EAAEU,MAAM,CAAC;YAC7BC,IAAIX,EAAEY,MAAM;YACZC,YAAYb,EAAEY,MAAM;YACpBE,QAAQd,EAAEY,MAAM,GAAGG,QAAQ;QAC7B;QAEA,MAAM,EAAEJ,EAAE,EAAEE,UAAU,EAAEC,MAAM,EAAE,GAAGL,cAAcO,KAAK,CAACR;QAEvD,MAAMS,WAAW,MAAMf,IAAIgB,OAAO,CAACC,QAAQ,CAAC;YAC1CR;YACAE;YACAO,OAAO;QACT;QAEA,IAAI,CAACH,UAAU;YACb,OAAOb,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAkB,GAAG;gBAAEC,QAAQ;YAAI;QACnE;QAEA,MAAMc,eAAenB,IAAIgB,OAAO,CAACI,MAAM,CAACC,MAAM,EAAEC;QAIhD,IAAI,CAACH,cAAc;YACjB,OAAOjB,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAA0B,GAAG;gBAAEC,QAAQ;YAAI;QAC3E;QAEA,IAAI,CAACc,aAAaI,iBAAiB,EAAE;YACnC,OAAOrB,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAA4C,GAAG;gBAAEC,QAAQ;YAAI;QAC7F;QAEA,IAAI,CAACc,aAAaK,QAAQ,EAAE;YAC1B,OAAOtB,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAkC,GAAG;gBAAEC,QAAQ;YAAI;QACnF;QAEA,0BAA0B;QAC1B,MAAMoB,eAAeb,UAAUO,aAAaP,MAAM;QAClD,IAAI,CAACa,cAAc;YACjB,OAAOvB,SAASC,IAAI,CAClB;gBACEC,OACE;YACJ,GACA;gBAAEC,QAAQ;YAAI;QAElB;QAEA,MAAMqB,oBAAoBP,aAAaI,iBAAiB,CAACR;QAEzD,MAAMY,SAAS,MAAMR,aAAaK,QAAQ,CAACI,OAAO,CAAC;YACjDC,UACE,cAAcd,YAAY,OAAOA,SAASc,QAAQ,KAAK,WACnDd,SAASc,QAAQ,GACjBC;YACNJ;YACAd,QAAQa;YACRzB;QACF;QAEA,IAAI,CAAC2B,OAAOI,OAAO,EAAE;YACnB,OAAO7B,SAASC,IAAI,CAClB;gBAAEC,OAAOuB,OAAOvB,KAAK,IAAI;YAA8B,GACvD;gBAAEC,QAAQ;YAAI;QAElB;QAEA,OAAOH,SAASC,IAAI,CAACwB,OAAOA,MAAM;IACpC,EAAE,OAAOvB,OAAO;QACd4B,QAAQ5B,KAAK,CAAC,8BAA8BA;QAC5C,OAAOF,SAASC,IAAI,CAClB;YACEC,OAAO,CAAC,2BAA2B,EAAEA,iBAAiB6B,QAAQ7B,MAAM8B,OAAO,GAAG,iBAAiB;QACjG,GACA;YAAE7B,QAAQ;QAAI;IAElB;AACF,EAAC"}
@@ -1,3 +1,3 @@
1
1
  export { AltTextField } from '../components/AltTextField.js';
2
- export { GenerateAltTextButton } from '../components/GenerateAltTextButton.js';
3
2
  export { BulkGenerateAltTextsButton } from '../components/BulkGenerateAltTextsButton.js';
3
+ export { GenerateAltTextButton } from '../components/GenerateAltTextButton.js';
@@ -1,5 +1,5 @@
1
1
  export { AltTextField } from '../components/AltTextField.js';
2
- export { GenerateAltTextButton } from '../components/GenerateAltTextButton.js';
3
2
  export { BulkGenerateAltTextsButton } from '../components/BulkGenerateAltTextsButton.js';
3
+ export { GenerateAltTextButton } from '../components/GenerateAltTextButton.js';
4
4
 
5
5
  //# sourceMappingURL=client.js.map
@@ -1 +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"}
1
+ {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { AltTextField } from '../components/AltTextField.js'\nexport { BulkGenerateAltTextsButton } from '../components/BulkGenerateAltTextsButton.js'\nexport { GenerateAltTextButton } from '../components/GenerateAltTextButton.js'\n"],"names":["AltTextField","BulkGenerateAltTextsButton","GenerateAltTextButton"],"mappings":"AAAA,SAASA,YAAY,QAAQ,gCAA+B;AAC5D,SAASC,0BAA0B,QAAQ,8CAA6C;AACxF,SAASC,qBAAqB,QAAQ,yCAAwC"}
@@ -2,26 +2,31 @@ import { translatedLabel } from '../utils/translatedLabel.js';
2
2
  export function altTextField({ localized }) {
3
3
  return {
4
4
  name: 'alt',
5
- label: translatedLabel('alternateText'),
6
5
  type: 'textarea',
7
- required: true,
8
- localized: localized,
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) {
12
- if (!value || value.trim().length === 0) {
13
- // @ts-expect-error - the translation key type does not include the custom key
14
- return t('@jhb.software/payload-alt-text-plugin:theAlternateTextIsRequired');
15
- }
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)
19
- return true;
20
- },
21
6
  admin: {
22
7
  components: {
23
8
  Field: '@jhb.software/payload-alt-text-plugin/client#AltTextField'
24
9
  }
10
+ },
11
+ label: translatedLabel('alternateText'),
12
+ localized,
13
+ required: true,
14
+ validate: (value, { data, operation, req: { t } })=>{
15
+ // Since https://github.com/payloadcms/payload/pull/14988, when using external storage (e.g., S3),
16
+ // it is no longer possible to detect whether this validation runs during the initial upload
17
+ // or a regular update by checking the existence of the ID.
18
+ // Instead, compare the timestamps of the createdAt and updatedAt fields.
19
+ const isInitialUpload = operation === 'create' || 'createdAt' in data && 'updatedAt' in data && data.createdAt === data.updatedAt;
20
+ // initial upload: allow without alt text
21
+ if (isInitialUpload) {
22
+ return true;
23
+ }
24
+ // regular update: require alt text
25
+ if (!value || value.trim().length === 0) {
26
+ // @ts-expect-error - the translation key type does not include the custom key
27
+ return t('@jhb.software/payload-alt-text-plugin:theAlternateTextIsRequired');
28
+ }
29
+ return true;
25
30
  }
26
31
  };
27
32
  }
@@ -1 +1 @@
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
+ {"version":3,"sources":["../../src/fields/altTextField.ts"],"sourcesContent":["import type { TextareaField } from 'payload'\n\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 type: 'textarea',\n admin: {\n components: {\n Field: '@jhb.software/payload-alt-text-plugin/client#AltTextField',\n },\n },\n label: translatedLabel('alternateText'),\n localized,\n required: true,\n validate: (value, { data, operation, req: { t } }) => {\n // Since https://github.com/payloadcms/payload/pull/14988, when using external storage (e.g., S3),\n // it is no longer possible to detect whether this validation runs during the initial upload\n // or a regular update by checking the existence of the ID.\n // Instead, compare the timestamps of the createdAt and updatedAt fields.\n const isInitialUpload =\n operation === 'create' ||\n ('createdAt' in data && 'updatedAt' in data && data.createdAt === data.updatedAt)\n\n // initial upload: allow without alt text\n if (isInitialUpload) {\n return true\n }\n\n // regular update: require alt text\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 return true\n },\n }\n}\n"],"names":["translatedLabel","altTextField","localized","name","type","admin","components","Field","label","required","validate","value","data","operation","req","t","isInitialUpload","createdAt","updatedAt","trim","length"],"mappings":"AAEA,SAASA,eAAe,QAAQ,8BAA6B;AAE7D,OAAO,SAASC,aAAa,EAC3BC,SAAS,EAGV;IACC,OAAO;QACLC,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,YAAY;gBACVC,OAAO;YACT;QACF;QACAC,OAAOR,gBAAgB;QACvBE;QACAO,UAAU;QACVC,UAAU,CAACC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,KAAK,EAAEC,CAAC,EAAE,EAAE;YAC/C,kGAAkG;YAClG,4FAA4F;YAC5F,2DAA2D;YAC3D,yEAAyE;YACzE,MAAMC,kBACJH,cAAc,YACb,eAAeD,QAAQ,eAAeA,QAAQA,KAAKK,SAAS,KAAKL,KAAKM,SAAS;YAElF,yCAAyC;YACzC,IAAIF,iBAAiB;gBACnB,OAAO;YACT;YAEA,mCAAmC;YACnC,IAAI,CAACL,SAASA,MAAMQ,IAAI,GAAGC,MAAM,KAAK,GAAG;gBACvC,8EAA8E;gBAC9E,OAAOL,EAAE;YACX;YAEA,OAAO;QACT;IACF;AACF"}
@@ -2,16 +2,16 @@ import { translatedLabel } from '../utils/translatedLabel.js';
2
2
  export function keywordsField({ localized }) {
3
3
  return {
4
4
  name: 'keywords',
5
- label: translatedLabel('keywords'),
6
5
  type: 'text',
7
- hasMany: true,
8
- required: false,
9
- localized: localized,
10
6
  admin: {
11
7
  description: 'Keywords which describe the image. Used when searching for the image.',
12
- readOnly: true,
13
- hidden: true
14
- }
8
+ hidden: true,
9
+ readOnly: true
10
+ },
11
+ hasMany: true,
12
+ label: translatedLabel('keywords'),
13
+ localized,
14
+ required: false
15
15
  };
16
16
  }
17
17
 
@@ -1 +1 @@
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"}
1
+ {"version":3,"sources":["../../src/fields/keywordsField.ts"],"sourcesContent":["import type { TextField } from 'payload'\n\nimport { translatedLabel } from '../utils/translatedLabel.js'\n\nexport function keywordsField({ localized }: { localized?: TextField['localized'] }): TextField {\n return {\n name: 'keywords',\n type: 'text',\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 hidden: true, // this field is only meant to be used for improving the search, therefore hide it from the UI\n readOnly: true,\n },\n hasMany: true,\n label: translatedLabel('keywords'),\n localized,\n required: false,\n }\n}\n"],"names":["translatedLabel","keywordsField","localized","name","type","admin","description","hidden","readOnly","hasMany","label","required"],"mappings":"AAEA,SAASA,eAAe,QAAQ,8BAA6B;AAE7D,OAAO,SAASC,cAAc,EAAEC,SAAS,EAA0C;IACjF,OAAO;QACLC,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;YACbC,QAAQ;YACRC,UAAU;QACZ;QACAC,SAAS;QACTC,OAAOV,gBAAgB;QACvBE;QACAS,UAAU;IACZ;AACF"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { payloadAltTextPlugin } from './plugin.js';
2
+ export { openAIResolver } from './resolvers/openAI.js';
3
+ export * from './resolvers/types.js';
2
4
  export type { IncomingAltTextPluginConfig as AltTextPluginConfig } from './types/AltTextPluginConfig.js';
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { payloadAltTextPlugin } from './plugin.js';
2
+ export { openAIResolver } from './resolvers/openAI.js';
3
+ export * from './resolvers/types.js';
2
4
 
3
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadAltTextPlugin } from './plugin.js'\nexport { openAIResolver } from './resolvers/openAI.js'\nexport * from './resolvers/types.js'\nexport type { IncomingAltTextPluginConfig as AltTextPluginConfig } from './types/AltTextPluginConfig.js'\n"],"names":["payloadAltTextPlugin","openAIResolver"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,cAAa;AAClD,SAASC,cAAc,QAAQ,wBAAuB;AACtD,cAAc,uBAAsB"}