@strapi/upload 5.33.4 → 5.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/dist/admin/components/EditAssetDialog/EditAssetContent.js +32 -3
  2. package/dist/admin/components/EditAssetDialog/EditAssetContent.js.map +1 -1
  3. package/dist/admin/components/EditAssetDialog/EditAssetContent.mjs +32 -3
  4. package/dist/admin/components/EditAssetDialog/EditAssetContent.mjs.map +1 -1
  5. package/dist/admin/components/EditAssetDialog/PreviewBox/AssetPreview.js.map +1 -1
  6. package/dist/admin/components/EditAssetDialog/PreviewBox/AssetPreview.mjs.map +1 -1
  7. package/dist/admin/components/EditAssetDialog/PreviewBox/FocalPointActions.js +57 -0
  8. package/dist/admin/components/EditAssetDialog/PreviewBox/FocalPointActions.js.map +1 -0
  9. package/dist/admin/components/EditAssetDialog/PreviewBox/FocalPointActions.mjs +55 -0
  10. package/dist/admin/components/EditAssetDialog/PreviewBox/FocalPointActions.mjs.map +1 -0
  11. package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewBox.js +96 -20
  12. package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewBox.js.map +1 -1
  13. package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewBox.mjs +98 -22
  14. package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewBox.mjs.map +1 -1
  15. package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewComponents.js +47 -0
  16. package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewComponents.js.map +1 -1
  17. package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewComponents.mjs +44 -1
  18. package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewComponents.mjs.map +1 -1
  19. package/dist/admin/future/App.js +45 -0
  20. package/dist/admin/future/App.js.map +1 -0
  21. package/dist/admin/future/App.mjs +43 -0
  22. package/dist/admin/future/App.mjs.map +1 -0
  23. package/dist/admin/future/pages/AIGenerationPage.js +24 -0
  24. package/dist/admin/future/pages/AIGenerationPage.js.map +1 -0
  25. package/dist/admin/future/pages/AIGenerationPage.mjs +22 -0
  26. package/dist/admin/future/pages/AIGenerationPage.mjs.map +1 -0
  27. package/dist/admin/future/pages/MediaLibraryPage.js +119 -0
  28. package/dist/admin/future/pages/MediaLibraryPage.js.map +1 -0
  29. package/dist/admin/future/pages/MediaLibraryPage.mjs +98 -0
  30. package/dist/admin/future/pages/MediaLibraryPage.mjs.map +1 -0
  31. package/dist/admin/future/services/api.js +28 -0
  32. package/dist/admin/future/services/api.js.map +1 -0
  33. package/dist/admin/future/services/api.mjs +25 -0
  34. package/dist/admin/future/services/api.mjs.map +1 -0
  35. package/dist/admin/future/utils/translations.js +8 -0
  36. package/dist/admin/future/utils/translations.js.map +1 -0
  37. package/dist/admin/future/utils/translations.mjs +6 -0
  38. package/dist/admin/future/utils/translations.mjs.map +1 -0
  39. package/dist/admin/hooks/useAIMetadataJob.js +114 -0
  40. package/dist/admin/hooks/useAIMetadataJob.js.map +1 -0
  41. package/dist/admin/hooks/useAIMetadataJob.mjs +93 -0
  42. package/dist/admin/hooks/useAIMetadataJob.mjs.map +1 -0
  43. package/dist/admin/hooks/useEditAsset.js +1 -0
  44. package/dist/admin/hooks/useEditAsset.js.map +1 -1
  45. package/dist/admin/hooks/useEditAsset.mjs +1 -0
  46. package/dist/admin/hooks/useEditAsset.mjs.map +1 -1
  47. package/dist/admin/index.js +23 -4
  48. package/dist/admin/index.js.map +1 -1
  49. package/dist/admin/index.mjs +24 -5
  50. package/dist/admin/index.mjs.map +1 -1
  51. package/dist/admin/package.json.js +6 -5
  52. package/dist/admin/package.json.js.map +1 -1
  53. package/dist/admin/package.json.mjs +6 -5
  54. package/dist/admin/package.json.mjs.map +1 -1
  55. package/dist/admin/pages/App/ConfigureTheView/ConfigureTheView.js +1 -0
  56. package/dist/admin/pages/App/ConfigureTheView/ConfigureTheView.js.map +1 -1
  57. package/dist/admin/pages/App/ConfigureTheView/ConfigureTheView.mjs +1 -0
  58. package/dist/admin/pages/App/ConfigureTheView/ConfigureTheView.mjs.map +1 -1
  59. package/dist/admin/pages/App/components/Header.js +3 -0
  60. package/dist/admin/pages/App/components/Header.js.map +1 -1
  61. package/dist/admin/pages/App/components/Header.mjs +3 -0
  62. package/dist/admin/pages/App/components/Header.mjs.map +1 -1
  63. package/dist/admin/pages/SettingsPage/SettingsPage.js +252 -67
  64. package/dist/admin/pages/SettingsPage/SettingsPage.js.map +1 -1
  65. package/dist/admin/pages/SettingsPage/SettingsPage.mjs +256 -71
  66. package/dist/admin/pages/SettingsPage/SettingsPage.mjs.map +1 -1
  67. package/dist/admin/src/components/EditAssetDialog/PreviewBox/AssetPreview.d.ts +1 -2
  68. package/dist/admin/src/components/EditAssetDialog/PreviewBox/FocalPointActions.d.ts +7 -0
  69. package/dist/admin/src/components/EditAssetDialog/PreviewBox/PreviewBox.d.ts +6 -2
  70. package/dist/admin/src/components/EditAssetDialog/PreviewBox/PreviewComponents.d.ts +13 -0
  71. package/dist/admin/src/future/App.d.ts +1 -0
  72. package/dist/admin/src/future/pages/AIGenerationPage.d.ts +1 -0
  73. package/dist/admin/src/future/pages/MediaLibraryPage.d.ts +1 -0
  74. package/dist/admin/src/future/services/api.d.ts +6 -0
  75. package/dist/admin/src/future/services/settings.d.ts +2 -0
  76. package/dist/admin/src/future/utils/translations.d.ts +1 -0
  77. package/dist/admin/src/hooks/useAIMetadataJob.d.ts +9 -0
  78. package/dist/admin/translations/de.json.js +44 -1
  79. package/dist/admin/translations/de.json.js.map +1 -1
  80. package/dist/admin/translations/de.json.mjs +44 -1
  81. package/dist/admin/translations/de.json.mjs.map +1 -1
  82. package/dist/admin/translations/en.json.js +17 -0
  83. package/dist/admin/translations/en.json.js.map +1 -1
  84. package/dist/admin/translations/en.json.mjs +17 -0
  85. package/dist/admin/translations/en.json.mjs.map +1 -1
  86. package/dist/server/bootstrap.js +1 -0
  87. package/dist/server/bootstrap.js.map +1 -1
  88. package/dist/server/bootstrap.mjs +1 -0
  89. package/dist/server/bootstrap.mjs.map +1 -1
  90. package/dist/server/content-types/file.js +4 -0
  91. package/dist/server/content-types/file.js.map +1 -1
  92. package/dist/server/content-types/file.mjs +4 -0
  93. package/dist/server/content-types/file.mjs.map +1 -1
  94. package/dist/server/controllers/admin-file.js +86 -0
  95. package/dist/server/controllers/admin-file.js.map +1 -1
  96. package/dist/server/controllers/admin-file.mjs +86 -0
  97. package/dist/server/controllers/admin-file.mjs.map +1 -1
  98. package/dist/server/controllers/admin-upload.js +3 -23
  99. package/dist/server/controllers/admin-upload.js.map +1 -1
  100. package/dist/server/controllers/admin-upload.mjs +3 -23
  101. package/dist/server/controllers/admin-upload.mjs.map +1 -1
  102. package/dist/server/controllers/validation/admin/upload.js +5 -0
  103. package/dist/server/controllers/validation/admin/upload.js.map +1 -1
  104. package/dist/server/controllers/validation/admin/upload.mjs +5 -0
  105. package/dist/server/controllers/validation/admin/upload.mjs.map +1 -1
  106. package/dist/server/controllers/validation/content-api/upload.js +6 -1
  107. package/dist/server/controllers/validation/content-api/upload.js.map +1 -1
  108. package/dist/server/controllers/validation/content-api/upload.mjs +6 -1
  109. package/dist/server/controllers/validation/content-api/upload.mjs.map +1 -1
  110. package/dist/server/models/ai-metadata-job.js +36 -0
  111. package/dist/server/models/ai-metadata-job.js.map +1 -0
  112. package/dist/server/models/ai-metadata-job.mjs +33 -0
  113. package/dist/server/models/ai-metadata-job.mjs.map +1 -0
  114. package/dist/server/register.js +3 -0
  115. package/dist/server/register.js.map +1 -1
  116. package/dist/server/register.mjs +3 -0
  117. package/dist/server/register.mjs.map +1 -1
  118. package/dist/server/routes/admin.js +46 -0
  119. package/dist/server/routes/admin.js.map +1 -1
  120. package/dist/server/routes/admin.mjs +46 -0
  121. package/dist/server/routes/admin.mjs.map +1 -1
  122. package/dist/server/services/ai-metadata-jobs.js +72 -0
  123. package/dist/server/services/ai-metadata-jobs.js.map +1 -0
  124. package/dist/server/services/ai-metadata-jobs.mjs +70 -0
  125. package/dist/server/services/ai-metadata-jobs.mjs.map +1 -0
  126. package/dist/server/services/ai-metadata.js +170 -20
  127. package/dist/server/services/ai-metadata.js.map +1 -1
  128. package/dist/server/services/ai-metadata.mjs +170 -20
  129. package/dist/server/services/ai-metadata.mjs.map +1 -1
  130. package/dist/server/services/index.js +3 -1
  131. package/dist/server/services/index.js.map +1 -1
  132. package/dist/server/services/index.mjs +3 -1
  133. package/dist/server/services/index.mjs.map +1 -1
  134. package/dist/server/services/upload.js +3 -1
  135. package/dist/server/services/upload.js.map +1 -1
  136. package/dist/server/services/upload.mjs +3 -1
  137. package/dist/server/services/upload.mjs.map +1 -1
  138. package/dist/server/src/bootstrap.d.ts.map +1 -1
  139. package/dist/server/src/content-types/file.d.ts +4 -0
  140. package/dist/server/src/content-types/file.d.ts.map +1 -1
  141. package/dist/server/src/content-types/index.d.ts +4 -0
  142. package/dist/server/src/content-types/index.d.ts.map +1 -1
  143. package/dist/server/src/controllers/admin-file.d.ts +3 -0
  144. package/dist/server/src/controllers/admin-file.d.ts.map +1 -1
  145. package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
  146. package/dist/server/src/controllers/index.d.ts +3 -0
  147. package/dist/server/src/controllers/index.d.ts.map +1 -1
  148. package/dist/server/src/controllers/validation/admin/upload.d.ts +240 -0
  149. package/dist/server/src/controllers/validation/admin/upload.d.ts.map +1 -1
  150. package/dist/server/src/controllers/validation/content-api/upload.d.ts +180 -0
  151. package/dist/server/src/controllers/validation/content-api/upload.d.ts.map +1 -1
  152. package/dist/server/src/index.d.ts +32 -2
  153. package/dist/server/src/index.d.ts.map +1 -1
  154. package/dist/server/src/models/ai-metadata-job.d.ts +5 -0
  155. package/dist/server/src/models/ai-metadata-job.d.ts.map +1 -0
  156. package/dist/server/src/models/index.d.ts +5 -0
  157. package/dist/server/src/models/index.d.ts.map +1 -0
  158. package/dist/server/src/register.d.ts.map +1 -1
  159. package/dist/server/src/routes/admin.d.ts.map +1 -1
  160. package/dist/server/src/services/ai-metadata-jobs.d.ts +14 -0
  161. package/dist/server/src/services/ai-metadata-jobs.d.ts.map +1 -0
  162. package/dist/server/src/services/ai-metadata.d.ts +25 -2
  163. package/dist/server/src/services/ai-metadata.d.ts.map +1 -1
  164. package/dist/server/src/services/index.d.ts +25 -2
  165. package/dist/server/src/services/index.d.ts.map +1 -1
  166. package/dist/server/src/services/upload.d.ts +1 -1
  167. package/dist/server/src/services/upload.d.ts.map +1 -1
  168. package/dist/server/src/types.d.ts +6 -0
  169. package/dist/server/src/types.d.ts.map +1 -1
  170. package/dist/server/src/utils/images.d.ts +7 -0
  171. package/dist/server/src/utils/images.d.ts.map +1 -0
  172. package/dist/server/src/utils/index.d.ts +2 -0
  173. package/dist/server/src/utils/index.d.ts.map +1 -1
  174. package/dist/server/utils/images.js +35 -0
  175. package/dist/server/utils/images.js.map +1 -0
  176. package/dist/server/utils/images.mjs +33 -0
  177. package/dist/server/utils/images.mjs.map +1 -0
  178. package/dist/server/utils/index.js.map +1 -1
  179. package/dist/server/utils/index.mjs.map +1 -1
  180. package/dist/shared/contracts/ai-metadata-jobs.d.ts +53 -0
  181. package/dist/shared/contracts/ai-metadata-jobs.d.ts.map +1 -0
  182. package/dist/shared/contracts/files.d.ts +39 -0
  183. package/package.json +6 -5
@@ -1,7 +1,19 @@
1
1
  'use strict';
2
2
 
3
3
  var zod = require('zod');
4
+ var index = require('../utils/index.js');
5
+ var images = require('../utils/images.js');
4
6
 
7
+ /**
8
+ * Supported image types for AI metadata generation
9
+ * @see https://ai.google.dev/gemini-api/docs/image-understanding
10
+ */ const SUPPORTED_IMAGE_TYPES = [
11
+ 'image/png',
12
+ 'image/jpeg',
13
+ 'image/webp',
14
+ 'image/heic',
15
+ 'image/heif'
16
+ ];
5
17
  const createAIMetadataService = ({ strapi })=>{
6
18
  const aiServerUrl = process.env.STRAPI_AI_URL || 'https://strapi-ai.apps.strapi.io';
7
19
  return {
@@ -21,7 +33,144 @@ const createAIMetadataService = ({ strapi })=>{
21
33
  const aiMetadata = settings.aiMetadata ?? true;
22
34
  return aiMetadata;
23
35
  },
24
- async processFiles (files) {
36
+ async countImagesWithoutMetadata () {
37
+ const imagesWithoutMetadataCountPromise = strapi.db.query('plugin::upload.file').count({
38
+ where: {
39
+ mime: {
40
+ $in: SUPPORTED_IMAGE_TYPES
41
+ },
42
+ $or: [
43
+ {
44
+ alternativeText: {
45
+ $null: true
46
+ }
47
+ },
48
+ {
49
+ alternativeText: ''
50
+ },
51
+ {
52
+ caption: {
53
+ $null: true
54
+ }
55
+ },
56
+ {
57
+ caption: ''
58
+ }
59
+ ]
60
+ }
61
+ });
62
+ const totalImagesPromise = strapi.db.query('plugin::upload.file').count({
63
+ where: {
64
+ mime: {
65
+ $in: SUPPORTED_IMAGE_TYPES
66
+ }
67
+ }
68
+ });
69
+ const [imagesWithoutMetadataCount, totalImages] = await Promise.all([
70
+ imagesWithoutMetadataCountPromise,
71
+ totalImagesPromise
72
+ ]);
73
+ return {
74
+ imagesWithoutMetadataCount,
75
+ totalImages
76
+ };
77
+ },
78
+ /**
79
+ * Update files with AI-generated metadata
80
+ * Shared logic used by both upload flow and retroactive processing
81
+ */ async updateFilesWithAIMetadata (files, metadataResults, user) {
82
+ const uploadService = strapi.plugin('upload').service('upload');
83
+ await Promise.all(files.map(async (file, index)=>{
84
+ const aiMetadata = metadataResults[index];
85
+ if (aiMetadata) {
86
+ // Only update fields that are missing (null or empty string)
87
+ const updateData = {};
88
+ if (!file.alternativeText || file.alternativeText === '') {
89
+ updateData.alternativeText = aiMetadata.altText;
90
+ }
91
+ if (!file.caption || file.caption === '') {
92
+ updateData.caption = aiMetadata.caption;
93
+ }
94
+ // Only update if there are fields to update
95
+ if (Object.keys(updateData).length > 0) {
96
+ await uploadService.updateFileInfo(file.id, updateData, {
97
+ user
98
+ });
99
+ // Update in-memory file object (needed for upload flow response)
100
+ if (updateData.alternativeText !== undefined) {
101
+ file.alternativeText = updateData.alternativeText;
102
+ }
103
+ if (updateData.caption !== undefined) {
104
+ file.caption = updateData.caption;
105
+ }
106
+ }
107
+ }
108
+ }));
109
+ },
110
+ /**
111
+ * Process existing files with job tracking for progress updates
112
+ */ async processExistingFiles (jobId, user) {
113
+ const jobService = index.getService('aiMetadataJobs');
114
+ try {
115
+ // Mark as processing
116
+ await jobService.updateJob(jobId, {
117
+ status: 'processing'
118
+ });
119
+ // Query all images without metadata
120
+ const files = await strapi.db.query('plugin::upload.file').findMany({
121
+ where: {
122
+ mime: {
123
+ $in: SUPPORTED_IMAGE_TYPES
124
+ },
125
+ $or: [
126
+ {
127
+ alternativeText: {
128
+ $null: true
129
+ }
130
+ },
131
+ {
132
+ alternativeText: ''
133
+ },
134
+ {
135
+ caption: {
136
+ $null: true
137
+ }
138
+ },
139
+ {
140
+ caption: ''
141
+ }
142
+ ]
143
+ }
144
+ });
145
+ if (files.length === 0) {
146
+ await jobService.updateJob(jobId, {
147
+ status: 'completed',
148
+ completedAt: new Date()
149
+ });
150
+ return;
151
+ }
152
+ // Process all files at once
153
+ const metadataResults = await this.processFiles(files);
154
+ await this.updateFilesWithAIMetadata(files, metadataResults, user);
155
+ // Mark as completed
156
+ await jobService.updateJob(jobId, {
157
+ status: 'completed',
158
+ completedAt: new Date()
159
+ });
160
+ } catch (error) {
161
+ strapi.log.error('AI metadata job failed', {
162
+ jobId,
163
+ error: error instanceof Error ? error.message : String(error)
164
+ });
165
+ await jobService.updateJob(jobId, {
166
+ status: 'failed',
167
+ completedAt: new Date()
168
+ });
169
+ }
170
+ },
171
+ /**
172
+ * Processes provided files for AI metadata generation
173
+ */ async processFiles (files) {
25
174
  if (!await this.isEnabled() || !aiServerUrl) {
26
175
  throw new Error('AI Metadata service is not enabled');
27
176
  }
@@ -30,27 +179,24 @@ const createAIMetadataService = ({ strapi })=>{
30
179
  const imageFiles = files.map((file, index)=>({
31
180
  file,
32
181
  originalIndex: index
33
- })).filter(({ file })=>file.mimetype?.startsWith('image/'));
182
+ })).filter(({ file })=>file.mime?.startsWith('image/'));
183
+ // Convert filtered image files to InputFile format (uses thumbnails when available)
184
+ const imageInputFiles = imageFiles.map(({ file })=>{
185
+ const thumbnail = file.formats?.thumbnail;
186
+ return {
187
+ filepath: thumbnail?.url || file.url || '',
188
+ mimetype: file.mime,
189
+ originalFilename: file.name,
190
+ size: thumbnail?.size || file.size,
191
+ provider: file.provider
192
+ };
193
+ });
34
194
  // If no image files, return sparse array with all nulls to avoid calling the AI server
35
195
  // This maintains the same array length as input files for proper index alignment
36
196
  if (imageFiles.length === 0) {
37
197
  return new Array(files.length).fill(null);
38
198
  }
39
- const formData = new FormData();
40
- for (const { file } of imageFiles){
41
- const fullUrl = file.provider === 'local' ? strapi.config.get('server.absoluteUrl') + file.filepath : file.filepath;
42
- const resp = await fetch(fullUrl);
43
- if (!resp.ok) {
44
- throw new Error(`Failed to fetch image from URL: ${fullUrl} (${resp.status})`);
45
- }
46
- const ab = await resp.arrayBuffer();
47
- const blob = new Blob([
48
- ab
49
- ], {
50
- type: file.mimetype || undefined
51
- });
52
- formData.append('files', blob);
53
- }
199
+ const formData = await images.buildFormDataFromFiles(imageInputFiles, strapi.config.get('server.absoluteUrl'), strapi.log);
54
200
  let token;
55
201
  try {
56
202
  const tokenData = await strapi.get('ai').getAiToken();
@@ -60,7 +206,10 @@ const createAIMetadataService = ({ strapi })=>{
60
206
  cause: error instanceof Error ? error : undefined
61
207
  });
62
208
  }
63
- strapi.log.http('Contacting AI Server for media metadata generation');
209
+ strapi.log.http('Contacting AI Server for media metadata generation', {
210
+ aiServerUrl,
211
+ imageCount: imageFiles.length
212
+ });
64
213
  const res = await fetch(`${aiServerUrl}/media-library/generate-metadata`, {
65
214
  method: 'POST',
66
215
  body: formData,
@@ -69,8 +218,9 @@ const createAIMetadataService = ({ strapi })=>{
69
218
  }
70
219
  });
71
220
  if (!res.ok) {
221
+ const errorText = await res.text();
72
222
  throw Error(`AI metadata generation failed`, {
73
- cause: await res.text()
223
+ cause: errorText
74
224
  });
75
225
  }
76
226
  const responseSchema = zod.z.object({
@@ -80,7 +230,7 @@ const createAIMetadataService = ({ strapi })=>{
80
230
  }))
81
231
  });
82
232
  const { results } = responseSchema.parse(await res.json());
83
- strapi.log.http(`Media metadata generated successfully for ${results.length} files`);
233
+ strapi.log.http(`AI generated metadata successfully for ${results.length} files`);
84
234
  // Create sparse array with results at original indices
85
235
  // Example: files=[img1, pdf, img2] -> imageFiles=[{img1, index:0}, {img2, index:2}]
86
236
  // AI results=[meta1, meta2] -> sparse=[meta1, null, meta2]
@@ -1 +1 @@
1
- {"version":3,"file":"ai-metadata.js","sources":["../../../server/src/services/ai-metadata.ts"],"sourcesContent":["import type { Core } from '@strapi/types';\nimport { z } from 'zod';\nimport { InputFile } from '../types';\nimport { Settings } from '../controllers/validation/admin/settings';\n\nconst createAIMetadataService = ({ strapi }: { strapi: Core.Strapi }) => {\n const aiServerUrl = process.env.STRAPI_AI_URL || 'https://strapi-ai.apps.strapi.io';\n\n return {\n async isEnabled() {\n // Check if user disabled AI features globally\n const isAIEnabled = strapi.config.get('admin.ai.enabled', true);\n if (!isAIEnabled) {\n return false;\n }\n\n // Check if the user's license grants access to AI features\n const hasAccess = strapi.ee.features.isEnabled('cms-ai');\n if (!hasAccess) {\n return false;\n }\n\n // Check if feature is specifically enabled, defaulting to true\n const settings: Settings = await strapi.plugin('upload').service('upload').getSettings();\n const aiMetadata: boolean = settings.aiMetadata ?? true;\n\n return aiMetadata;\n },\n\n async processFiles(\n files: InputFile[]\n ): Promise<Array<{ altText: string; caption: string } | null>> {\n if (!(await this.isEnabled()) || !aiServerUrl) {\n throw new Error('AI Metadata service is not enabled');\n }\n\n // Filter for image files only and track their original positions\n // We need to maintain the original indices so we can map AI results back correctly\n const imageFiles = files\n .map((file, index) => ({ file, originalIndex: index }))\n .filter(({ file }) => file.mimetype?.startsWith('image/'));\n\n // If no image files, return sparse array with all nulls to avoid calling the AI server\n // This maintains the same array length as input files for proper index alignment\n if (imageFiles.length === 0) {\n return new Array(files.length).fill(null);\n }\n\n const formData = new FormData();\n\n for (const { file } of imageFiles) {\n const fullUrl =\n file.provider === 'local'\n ? strapi.config.get('server.absoluteUrl') + file.filepath\n : file.filepath;\n\n const resp = await fetch(fullUrl);\n if (!resp.ok) {\n throw new Error(`Failed to fetch image from URL: ${fullUrl} (${resp.status})`);\n }\n const ab = await resp.arrayBuffer();\n const blob: Blob = new Blob([ab], { type: file.mimetype || undefined });\n formData.append('files', blob);\n }\n\n let token: string;\n try {\n const tokenData = await strapi.get('ai').getAiToken();\n token = tokenData.token;\n } catch (error) {\n throw new Error('Failed to retrieve AI token', {\n cause: error instanceof Error ? error : undefined,\n });\n }\n\n strapi.log.http('Contacting AI Server for media metadata generation');\n const res = await fetch(`${aiServerUrl}/media-library/generate-metadata`, {\n method: 'POST',\n body: formData,\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!res.ok) {\n throw Error(`AI metadata generation failed`, { cause: await res.text() });\n }\n\n const responseSchema = z.object({\n results: z.array(\n z.object({\n altText: z.string(),\n caption: z.string(),\n })\n ),\n });\n\n const { results } = responseSchema.parse(await res.json());\n strapi.log.http(`Media metadata generated successfully for ${results.length} files`);\n\n // Create sparse array with results at original indices\n // Example: files=[img1, pdf, img2] -> imageFiles=[{img1, index:0}, {img2, index:2}]\n // AI results=[meta1, meta2] -> sparse=[meta1, null, meta2]\n // This ensures metadata[i] corresponds to files[i], with null for non-images\n return imageFiles.reduce((sparseResults, { originalIndex }, resultIndex) => {\n sparseResults[originalIndex] = results[resultIndex];\n return sparseResults;\n }, new Array(files.length).fill(null));\n },\n };\n};\n\nexport { createAIMetadataService };\n"],"names":["createAIMetadataService","strapi","aiServerUrl","process","env","STRAPI_AI_URL","isEnabled","isAIEnabled","config","get","hasAccess","ee","features","settings","plugin","service","getSettings","aiMetadata","processFiles","files","Error","imageFiles","map","file","index","originalIndex","filter","mimetype","startsWith","length","Array","fill","formData","FormData","fullUrl","provider","filepath","resp","fetch","ok","status","ab","arrayBuffer","blob","Blob","type","undefined","append","token","tokenData","getAiToken","error","cause","log","http","res","method","body","headers","Authorization","text","responseSchema","z","object","results","array","altText","string","caption","parse","json","reduce","sparseResults","resultIndex"],"mappings":"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"}
1
+ {"version":3,"file":"ai-metadata.js","sources":["../../../server/src/services/ai-metadata.ts"],"sourcesContent":["import type { Core } from '@strapi/types';\nimport { z } from 'zod';\nimport { InputFile, File } from '../types';\nimport { Settings } from '../controllers/validation/admin/settings';\nimport { getService } from '../utils';\nimport { buildFormDataFromFiles } from '../utils/images';\n\n/**\n * Supported image types for AI metadata generation\n * @see https://ai.google.dev/gemini-api/docs/image-understanding\n */\nconst SUPPORTED_IMAGE_TYPES = [\n 'image/png',\n 'image/jpeg',\n 'image/webp',\n 'image/heic',\n 'image/heif',\n] as const;\n\nconst createAIMetadataService = ({ strapi }: { strapi: Core.Strapi }) => {\n const aiServerUrl = process.env.STRAPI_AI_URL || 'https://strapi-ai.apps.strapi.io';\n\n return {\n async isEnabled() {\n // Check if user disabled AI features globally\n const isAIEnabled = strapi.config.get('admin.ai.enabled', true);\n if (!isAIEnabled) {\n return false;\n }\n\n // Check if the user's license grants access to AI features\n const hasAccess = strapi.ee.features.isEnabled('cms-ai');\n if (!hasAccess) {\n return false;\n }\n\n // Check if feature is specifically enabled, defaulting to true\n const settings: Settings = await strapi.plugin('upload').service('upload').getSettings();\n const aiMetadata: boolean = settings.aiMetadata ?? true;\n\n return aiMetadata;\n },\n\n async countImagesWithoutMetadata() {\n const imagesWithoutMetadataCountPromise = strapi.db.query('plugin::upload.file').count({\n where: {\n mime: {\n $in: SUPPORTED_IMAGE_TYPES,\n },\n $or: [\n { alternativeText: { $null: true } },\n { alternativeText: '' },\n { caption: { $null: true } },\n { caption: '' },\n ],\n },\n });\n\n const totalImagesPromise = strapi.db.query('plugin::upload.file').count({\n where: {\n mime: {\n $in: SUPPORTED_IMAGE_TYPES,\n },\n },\n });\n\n const [imagesWithoutMetadataCount, totalImages] = await Promise.all([\n imagesWithoutMetadataCountPromise,\n totalImagesPromise,\n ]);\n\n return { imagesWithoutMetadataCount, totalImages };\n },\n\n /**\n * Update files with AI-generated metadata\n * Shared logic used by both upload flow and retroactive processing\n */\n async updateFilesWithAIMetadata(\n files: File[],\n metadataResults: Array<{ altText: string; caption: string } | null>,\n user: { id: string | number }\n ) {\n const uploadService = strapi.plugin('upload').service('upload');\n\n await Promise.all(\n files.map(async (file, index) => {\n const aiMetadata = metadataResults[index];\n if (aiMetadata) {\n // Only update fields that are missing (null or empty string)\n const updateData: { alternativeText?: string; caption?: string } = {};\n\n if (!file.alternativeText || file.alternativeText === '') {\n updateData.alternativeText = aiMetadata.altText;\n }\n\n if (!file.caption || file.caption === '') {\n updateData.caption = aiMetadata.caption;\n }\n\n // Only update if there are fields to update\n if (Object.keys(updateData).length > 0) {\n await uploadService.updateFileInfo(file.id, updateData, { user });\n\n // Update in-memory file object (needed for upload flow response)\n if (updateData.alternativeText !== undefined) {\n file.alternativeText = updateData.alternativeText;\n }\n if (updateData.caption !== undefined) {\n file.caption = updateData.caption;\n }\n }\n }\n })\n );\n },\n\n /**\n * Process existing files with job tracking for progress updates\n */\n async processExistingFiles(jobId: number, user: { id: string | number }): Promise<void> {\n const jobService = getService('aiMetadataJobs');\n\n try {\n // Mark as processing\n await jobService.updateJob(jobId, { status: 'processing' });\n\n // Query all images without metadata\n const files: File[] = await strapi.db.query('plugin::upload.file').findMany({\n where: {\n mime: {\n $in: SUPPORTED_IMAGE_TYPES,\n },\n $or: [\n { alternativeText: { $null: true } },\n { alternativeText: '' },\n { caption: { $null: true } },\n { caption: '' },\n ],\n },\n });\n\n if (files.length === 0) {\n await jobService.updateJob(jobId, {\n status: 'completed',\n completedAt: new Date(),\n });\n return;\n }\n\n // Process all files at once\n const metadataResults = await this.processFiles(files);\n await this.updateFilesWithAIMetadata(files, metadataResults, user);\n\n // Mark as completed\n await jobService.updateJob(jobId, {\n status: 'completed',\n completedAt: new Date(),\n });\n } catch (error) {\n strapi.log.error('AI metadata job failed', {\n jobId,\n error: error instanceof Error ? error.message : String(error),\n });\n\n await jobService.updateJob(jobId, {\n status: 'failed',\n completedAt: new Date(),\n });\n }\n },\n\n /**\n * Processes provided files for AI metadata generation\n */\n async processFiles(files: File[]): Promise<Array<{ altText: string; caption: string } | null>> {\n if (!(await this.isEnabled()) || !aiServerUrl) {\n throw new Error('AI Metadata service is not enabled');\n }\n\n // Filter for image files only and track their original positions\n // We need to maintain the original indices so we can map AI results back correctly\n const imageFiles = files\n .map((file, index) => ({ file, originalIndex: index }))\n .filter(({ file }) => file.mime?.startsWith('image/'));\n\n // Convert filtered image files to InputFile format (uses thumbnails when available)\n const imageInputFiles = imageFiles.map(({ file }) => {\n const thumbnail = (file.formats as any)?.thumbnail;\n return {\n filepath: thumbnail?.url || file.url || '',\n mimetype: file.mime,\n originalFilename: file.name,\n size: thumbnail?.size || file.size,\n provider: file.provider,\n } as InputFile;\n });\n\n // If no image files, return sparse array with all nulls to avoid calling the AI server\n // This maintains the same array length as input files for proper index alignment\n if (imageFiles.length === 0) {\n return new Array(files.length).fill(null);\n }\n\n const formData = await buildFormDataFromFiles(\n imageInputFiles,\n strapi.config.get('server.absoluteUrl'),\n strapi.log\n );\n\n let token: string;\n try {\n const tokenData = await strapi.get('ai').getAiToken();\n token = tokenData.token;\n } catch (error) {\n throw new Error('Failed to retrieve AI token', {\n cause: error instanceof Error ? error : undefined,\n });\n }\n\n strapi.log.http('Contacting AI Server for media metadata generation', {\n aiServerUrl,\n imageCount: imageFiles.length,\n });\n\n const res = await fetch(`${aiServerUrl}/media-library/generate-metadata`, {\n method: 'POST',\n body: formData,\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!res.ok) {\n const errorText = await res.text();\n throw Error(`AI metadata generation failed`, { cause: errorText });\n }\n\n const responseSchema = z.object({\n results: z.array(\n z.object({\n altText: z.string(),\n caption: z.string(),\n })\n ),\n });\n\n const { results } = responseSchema.parse(await res.json());\n strapi.log.http(`AI generated metadata successfully for ${results.length} files`);\n\n // Create sparse array with results at original indices\n // Example: files=[img1, pdf, img2] -> imageFiles=[{img1, index:0}, {img2, index:2}]\n // AI results=[meta1, meta2] -> sparse=[meta1, null, meta2]\n // This ensures metadata[i] corresponds to files[i], with null for non-images\n return imageFiles.reduce((sparseResults, { originalIndex }, resultIndex) => {\n sparseResults[originalIndex] = results[resultIndex];\n return sparseResults;\n }, new Array(files.length).fill(null));\n },\n };\n};\n\nexport { createAIMetadataService };\n"],"names":["SUPPORTED_IMAGE_TYPES","createAIMetadataService","strapi","aiServerUrl","process","env","STRAPI_AI_URL","isEnabled","isAIEnabled","config","get","hasAccess","ee","features","settings","plugin","service","getSettings","aiMetadata","countImagesWithoutMetadata","imagesWithoutMetadataCountPromise","db","query","count","where","mime","$in","$or","alternativeText","$null","caption","totalImagesPromise","imagesWithoutMetadataCount","totalImages","Promise","all","updateFilesWithAIMetadata","files","metadataResults","user","uploadService","map","file","index","updateData","altText","Object","keys","length","updateFileInfo","id","undefined","processExistingFiles","jobId","jobService","getService","updateJob","status","findMany","completedAt","Date","processFiles","error","log","Error","message","String","imageFiles","originalIndex","filter","startsWith","imageInputFiles","thumbnail","formats","filepath","url","mimetype","originalFilename","name","size","provider","Array","fill","formData","buildFormDataFromFiles","token","tokenData","getAiToken","cause","http","imageCount","res","fetch","method","body","headers","Authorization","ok","errorText","text","responseSchema","z","object","results","array","string","parse","json","reduce","sparseResults","resultIndex"],"mappings":";;;;;;AAOA;;;AAGC,IACD,MAAMA,qBAAwB,GAAA;AAC5B,IAAA,WAAA;AACA,IAAA,YAAA;AACA,IAAA,YAAA;AACA,IAAA,YAAA;AACA,IAAA;AACD,CAAA;AAED,MAAMC,uBAA0B,GAAA,CAAC,EAAEC,MAAM,EAA2B,GAAA;AAClE,IAAA,MAAMC,WAAcC,GAAAA,OAAAA,CAAQC,GAAG,CAACC,aAAa,IAAI,kCAAA;IAEjD,OAAO;QACL,MAAMC,SAAAA,CAAAA,GAAAA;;AAEJ,YAAA,MAAMC,cAAcN,MAAOO,CAAAA,MAAM,CAACC,GAAG,CAAC,kBAAoB,EAAA,IAAA,CAAA;AAC1D,YAAA,IAAI,CAACF,WAAa,EAAA;gBAChB,OAAO,KAAA;AACT;;AAGA,YAAA,MAAMG,YAAYT,MAAOU,CAAAA,EAAE,CAACC,QAAQ,CAACN,SAAS,CAAC,QAAA,CAAA;AAC/C,YAAA,IAAI,CAACI,SAAW,EAAA;gBACd,OAAO,KAAA;AACT;;YAGA,MAAMG,QAAAA,GAAqB,MAAMZ,MAAOa,CAAAA,MAAM,CAAC,QAAUC,CAAAA,CAAAA,OAAO,CAAC,QAAA,CAAA,CAAUC,WAAW,EAAA;YACtF,MAAMC,UAAAA,GAAsBJ,QAASI,CAAAA,UAAU,IAAI,IAAA;YAEnD,OAAOA,UAAAA;AACT,SAAA;QAEA,MAAMC,0BAAAA,CAAAA,GAAAA;YACJ,MAAMC,iCAAAA,GAAoClB,OAAOmB,EAAE,CAACC,KAAK,CAAC,qBAAA,CAAA,CAAuBC,KAAK,CAAC;gBACrFC,KAAO,EAAA;oBACLC,IAAM,EAAA;wBACJC,GAAK1B,EAAAA;AACP,qBAAA;oBACA2B,GAAK,EAAA;AACH,wBAAA;4BAAEC,eAAiB,EAAA;gCAAEC,KAAO,EAAA;AAAK;AAAE,yBAAA;AACnC,wBAAA;4BAAED,eAAiB,EAAA;AAAG,yBAAA;AACtB,wBAAA;4BAAEE,OAAS,EAAA;gCAAED,KAAO,EAAA;AAAK;AAAE,yBAAA;AAC3B,wBAAA;4BAAEC,OAAS,EAAA;AAAG;AACf;AACH;AACF,aAAA,CAAA;YAEA,MAAMC,kBAAAA,GAAqB7B,OAAOmB,EAAE,CAACC,KAAK,CAAC,qBAAA,CAAA,CAAuBC,KAAK,CAAC;gBACtEC,KAAO,EAAA;oBACLC,IAAM,EAAA;wBACJC,GAAK1B,EAAAA;AACP;AACF;AACF,aAAA,CAAA;AAEA,YAAA,MAAM,CAACgC,0BAA4BC,EAAAA,WAAAA,CAAY,GAAG,MAAMC,OAAAA,CAAQC,GAAG,CAAC;AAClEf,gBAAAA,iCAAAA;AACAW,gBAAAA;AACD,aAAA,CAAA;YAED,OAAO;AAAEC,gBAAAA,0BAAAA;AAA4BC,gBAAAA;AAAY,aAAA;AACnD,SAAA;AAEA;;;AAGC,QACD,MAAMG,yBACJC,CAAAA,CAAAA,KAAa,EACbC,eAAmE,EACnEC,IAA6B,EAAA;AAE7B,YAAA,MAAMC,gBAAgBtC,MAAOa,CAAAA,MAAM,CAAC,QAAA,CAAA,CAAUC,OAAO,CAAC,QAAA,CAAA;AAEtD,YAAA,MAAMkB,QAAQC,GAAG,CACfE,MAAMI,GAAG,CAAC,OAAOC,IAAMC,EAAAA,KAAAA,GAAAA;gBACrB,MAAMzB,UAAAA,GAAaoB,eAAe,CAACK,KAAM,CAAA;AACzC,gBAAA,IAAIzB,UAAY,EAAA;;AAEd,oBAAA,MAAM0B,aAA6D,EAAC;AAEpE,oBAAA,IAAI,CAACF,IAAKd,CAAAA,eAAe,IAAIc,IAAKd,CAAAA,eAAe,KAAK,EAAI,EAAA;wBACxDgB,UAAWhB,CAAAA,eAAe,GAAGV,UAAAA,CAAW2B,OAAO;AACjD;AAEA,oBAAA,IAAI,CAACH,IAAKZ,CAAAA,OAAO,IAAIY,IAAKZ,CAAAA,OAAO,KAAK,EAAI,EAAA;wBACxCc,UAAWd,CAAAA,OAAO,GAAGZ,UAAAA,CAAWY,OAAO;AACzC;;AAGA,oBAAA,IAAIgB,OAAOC,IAAI,CAACH,UAAYI,CAAAA,CAAAA,MAAM,GAAG,CAAG,EAAA;AACtC,wBAAA,MAAMR,cAAcS,cAAc,CAACP,IAAKQ,CAAAA,EAAE,EAAEN,UAAY,EAAA;AAAEL,4BAAAA;AAAK,yBAAA,CAAA;;wBAG/D,IAAIK,UAAAA,CAAWhB,eAAe,KAAKuB,SAAW,EAAA;4BAC5CT,IAAKd,CAAAA,eAAe,GAAGgB,UAAAA,CAAWhB,eAAe;AACnD;wBACA,IAAIgB,UAAAA,CAAWd,OAAO,KAAKqB,SAAW,EAAA;4BACpCT,IAAKZ,CAAAA,OAAO,GAAGc,UAAAA,CAAWd,OAAO;AACnC;AACF;AACF;AACF,aAAA,CAAA,CAAA;AAEJ,SAAA;AAEA;;AAEC,QACD,MAAMsB,oBAAAA,CAAAA,CAAqBC,KAAa,EAAEd,IAA6B,EAAA;AACrE,YAAA,MAAMe,aAAaC,gBAAW,CAAA,gBAAA,CAAA;YAE9B,IAAI;;gBAEF,MAAMD,UAAAA,CAAWE,SAAS,CAACH,KAAO,EAAA;oBAAEI,MAAQ,EAAA;AAAa,iBAAA,CAAA;;gBAGzD,MAAMpB,KAAAA,GAAgB,MAAMnC,MAAOmB,CAAAA,EAAE,CAACC,KAAK,CAAC,qBAAuBoC,CAAAA,CAAAA,QAAQ,CAAC;oBAC1ElC,KAAO,EAAA;wBACLC,IAAM,EAAA;4BACJC,GAAK1B,EAAAA;AACP,yBAAA;wBACA2B,GAAK,EAAA;AACH,4BAAA;gCAAEC,eAAiB,EAAA;oCAAEC,KAAO,EAAA;AAAK;AAAE,6BAAA;AACnC,4BAAA;gCAAED,eAAiB,EAAA;AAAG,6BAAA;AACtB,4BAAA;gCAAEE,OAAS,EAAA;oCAAED,KAAO,EAAA;AAAK;AAAE,6BAAA;AAC3B,4BAAA;gCAAEC,OAAS,EAAA;AAAG;AACf;AACH;AACF,iBAAA,CAAA;gBAEA,IAAIO,KAAAA,CAAMW,MAAM,KAAK,CAAG,EAAA;oBACtB,MAAMM,UAAAA,CAAWE,SAAS,CAACH,KAAO,EAAA;wBAChCI,MAAQ,EAAA,WAAA;AACRE,wBAAAA,WAAAA,EAAa,IAAIC,IAAAA;AACnB,qBAAA,CAAA;AACA,oBAAA;AACF;;AAGA,gBAAA,MAAMtB,eAAkB,GAAA,MAAM,IAAI,CAACuB,YAAY,CAACxB,KAAAA,CAAAA;AAChD,gBAAA,MAAM,IAAI,CAACD,yBAAyB,CAACC,OAAOC,eAAiBC,EAAAA,IAAAA,CAAAA;;gBAG7D,MAAMe,UAAAA,CAAWE,SAAS,CAACH,KAAO,EAAA;oBAChCI,MAAQ,EAAA,WAAA;AACRE,oBAAAA,WAAAA,EAAa,IAAIC,IAAAA;AACnB,iBAAA,CAAA;AACF,aAAA,CAAE,OAAOE,KAAO,EAAA;AACd5D,gBAAAA,MAAAA,CAAO6D,GAAG,CAACD,KAAK,CAAC,wBAA0B,EAAA;AACzCT,oBAAAA,KAAAA;AACAS,oBAAAA,KAAAA,EAAOA,KAAiBE,YAAAA,KAAAA,GAAQF,KAAMG,CAAAA,OAAO,GAAGC,MAAOJ,CAAAA,KAAAA;AACzD,iBAAA,CAAA;gBAEA,MAAMR,UAAAA,CAAWE,SAAS,CAACH,KAAO,EAAA;oBAChCI,MAAQ,EAAA,QAAA;AACRE,oBAAAA,WAAAA,EAAa,IAAIC,IAAAA;AACnB,iBAAA,CAAA;AACF;AACF,SAAA;AAEA;;QAGA,MAAMC,cAAaxB,KAAa,EAAA;AAC9B,YAAA,IAAI,CAAE,MAAM,IAAI,CAAC9B,SAAS,EAAA,IAAO,CAACJ,WAAa,EAAA;AAC7C,gBAAA,MAAM,IAAI6D,KAAM,CAAA,oCAAA,CAAA;AAClB;;;AAIA,YAAA,MAAMG,aAAa9B,KAChBI,CAAAA,GAAG,CAAC,CAACC,IAAAA,EAAMC,SAAW;AAAED,oBAAAA,IAAAA;oBAAM0B,aAAezB,EAAAA;iBAAM,CAAA,CAAA,CACnD0B,MAAM,CAAC,CAAC,EAAE3B,IAAI,EAAE,GAAKA,IAAAA,CAAKjB,IAAI,EAAE6C,UAAW,CAAA,QAAA,CAAA,CAAA;;AAG9C,YAAA,MAAMC,kBAAkBJ,UAAW1B,CAAAA,GAAG,CAAC,CAAC,EAAEC,IAAI,EAAE,GAAA;gBAC9C,MAAM8B,SAAAA,GAAa9B,IAAK+B,CAAAA,OAAO,EAAUD,SAAAA;gBACzC,OAAO;AACLE,oBAAAA,QAAAA,EAAUF,SAAWG,EAAAA,GAAAA,IAAOjC,IAAKiC,CAAAA,GAAG,IAAI,EAAA;AACxCC,oBAAAA,QAAAA,EAAUlC,KAAKjB,IAAI;AACnBoD,oBAAAA,gBAAAA,EAAkBnC,KAAKoC,IAAI;oBAC3BC,IAAMP,EAAAA,SAAAA,EAAWO,IAAQrC,IAAAA,IAAAA,CAAKqC,IAAI;AAClCC,oBAAAA,QAAAA,EAAUtC,KAAKsC;AACjB,iBAAA;AACF,aAAA,CAAA;;;YAIA,IAAIb,UAAAA,CAAWnB,MAAM,KAAK,CAAG,EAAA;AAC3B,gBAAA,OAAO,IAAIiC,KAAM5C,CAAAA,KAAAA,CAAMW,MAAM,CAAA,CAAEkC,IAAI,CAAC,IAAA,CAAA;AACtC;YAEA,MAAMC,QAAAA,GAAW,MAAMC,6BAAAA,CACrBb,eACArE,EAAAA,MAAAA,CAAOO,MAAM,CAACC,GAAG,CAAC,oBAClBR,CAAAA,EAAAA,MAAAA,CAAO6D,GAAG,CAAA;YAGZ,IAAIsB,KAAAA;YACJ,IAAI;AACF,gBAAA,MAAMC,YAAY,MAAMpF,MAAAA,CAAOQ,GAAG,CAAC,MAAM6E,UAAU,EAAA;AACnDF,gBAAAA,KAAAA,GAAQC,UAAUD,KAAK;AACzB,aAAA,CAAE,OAAOvB,KAAO,EAAA;gBACd,MAAM,IAAIE,MAAM,6BAA+B,EAAA;oBAC7CwB,KAAO1B,EAAAA,KAAAA,YAAiBE,QAAQF,KAAQX,GAAAA;AAC1C,iBAAA,CAAA;AACF;AAEAjD,YAAAA,MAAAA,CAAO6D,GAAG,CAAC0B,IAAI,CAAC,oDAAsD,EAAA;AACpEtF,gBAAAA,WAAAA;AACAuF,gBAAAA,UAAAA,EAAYvB,WAAWnB;AACzB,aAAA,CAAA;AAEA,YAAA,MAAM2C,MAAM,MAAMC,KAAAA,CAAM,GAAGzF,WAAY,CAAA,gCAAgC,CAAC,EAAE;gBACxE0F,MAAQ,EAAA,MAAA;gBACRC,IAAMX,EAAAA,QAAAA;gBACNY,OAAS,EAAA;oBACPC,aAAe,EAAA,CAAC,OAAO,EAAEX,KAAO,CAAA;AAClC;AACF,aAAA,CAAA;YAEA,IAAI,CAACM,GAAIM,CAAAA,EAAE,EAAE;gBACX,MAAMC,SAAAA,GAAY,MAAMP,GAAAA,CAAIQ,IAAI,EAAA;AAChC,gBAAA,MAAMnC,KAAM,CAAA,CAAC,6BAA6B,CAAC,EAAE;oBAAEwB,KAAOU,EAAAA;AAAU,iBAAA,CAAA;AAClE;YAEA,MAAME,cAAAA,GAAiBC,KAAEC,CAAAA,MAAM,CAAC;AAC9BC,gBAAAA,OAAAA,EAASF,KAAEG,CAAAA,KAAK,CACdH,KAAAA,CAAEC,MAAM,CAAC;AACPzD,oBAAAA,OAAAA,EAASwD,MAAEI,MAAM,EAAA;AACjB3E,oBAAAA,OAAAA,EAASuE,MAAEI,MAAM;AACnB,iBAAA,CAAA;AAEJ,aAAA,CAAA;YAEA,MAAM,EAAEF,OAAO,EAAE,GAAGH,eAAeM,KAAK,CAAC,MAAMf,GAAAA,CAAIgB,IAAI,EAAA,CAAA;YACvDzG,MAAO6D,CAAAA,GAAG,CAAC0B,IAAI,CAAC,CAAC,uCAAuC,EAAEc,OAAQvD,CAAAA,MAAM,CAAC,MAAM,CAAC,CAAA;;;;;YAMhF,OAAOmB,UAAAA,CAAWyC,MAAM,CAAC,CAACC,eAAe,EAAEzC,aAAa,EAAE,EAAE0C,WAAAA,GAAAA;AAC1DD,gBAAAA,aAAa,CAACzC,aAAAA,CAAc,GAAGmC,OAAO,CAACO,WAAY,CAAA;gBACnD,OAAOD,aAAAA;AACT,aAAA,EAAG,IAAI5B,KAAM5C,CAAAA,KAAAA,CAAMW,MAAM,CAAA,CAAEkC,IAAI,CAAC,IAAA,CAAA,CAAA;AAClC;AACF,KAAA;AACF;;;;"}
@@ -1,5 +1,17 @@
1
1
  import { z } from 'zod';
2
+ import { getService } from '../utils/index.mjs';
3
+ import { buildFormDataFromFiles } from '../utils/images.mjs';
2
4
 
5
+ /**
6
+ * Supported image types for AI metadata generation
7
+ * @see https://ai.google.dev/gemini-api/docs/image-understanding
8
+ */ const SUPPORTED_IMAGE_TYPES = [
9
+ 'image/png',
10
+ 'image/jpeg',
11
+ 'image/webp',
12
+ 'image/heic',
13
+ 'image/heif'
14
+ ];
3
15
  const createAIMetadataService = ({ strapi })=>{
4
16
  const aiServerUrl = process.env.STRAPI_AI_URL || 'https://strapi-ai.apps.strapi.io';
5
17
  return {
@@ -19,7 +31,144 @@ const createAIMetadataService = ({ strapi })=>{
19
31
  const aiMetadata = settings.aiMetadata ?? true;
20
32
  return aiMetadata;
21
33
  },
22
- async processFiles (files) {
34
+ async countImagesWithoutMetadata () {
35
+ const imagesWithoutMetadataCountPromise = strapi.db.query('plugin::upload.file').count({
36
+ where: {
37
+ mime: {
38
+ $in: SUPPORTED_IMAGE_TYPES
39
+ },
40
+ $or: [
41
+ {
42
+ alternativeText: {
43
+ $null: true
44
+ }
45
+ },
46
+ {
47
+ alternativeText: ''
48
+ },
49
+ {
50
+ caption: {
51
+ $null: true
52
+ }
53
+ },
54
+ {
55
+ caption: ''
56
+ }
57
+ ]
58
+ }
59
+ });
60
+ const totalImagesPromise = strapi.db.query('plugin::upload.file').count({
61
+ where: {
62
+ mime: {
63
+ $in: SUPPORTED_IMAGE_TYPES
64
+ }
65
+ }
66
+ });
67
+ const [imagesWithoutMetadataCount, totalImages] = await Promise.all([
68
+ imagesWithoutMetadataCountPromise,
69
+ totalImagesPromise
70
+ ]);
71
+ return {
72
+ imagesWithoutMetadataCount,
73
+ totalImages
74
+ };
75
+ },
76
+ /**
77
+ * Update files with AI-generated metadata
78
+ * Shared logic used by both upload flow and retroactive processing
79
+ */ async updateFilesWithAIMetadata (files, metadataResults, user) {
80
+ const uploadService = strapi.plugin('upload').service('upload');
81
+ await Promise.all(files.map(async (file, index)=>{
82
+ const aiMetadata = metadataResults[index];
83
+ if (aiMetadata) {
84
+ // Only update fields that are missing (null or empty string)
85
+ const updateData = {};
86
+ if (!file.alternativeText || file.alternativeText === '') {
87
+ updateData.alternativeText = aiMetadata.altText;
88
+ }
89
+ if (!file.caption || file.caption === '') {
90
+ updateData.caption = aiMetadata.caption;
91
+ }
92
+ // Only update if there are fields to update
93
+ if (Object.keys(updateData).length > 0) {
94
+ await uploadService.updateFileInfo(file.id, updateData, {
95
+ user
96
+ });
97
+ // Update in-memory file object (needed for upload flow response)
98
+ if (updateData.alternativeText !== undefined) {
99
+ file.alternativeText = updateData.alternativeText;
100
+ }
101
+ if (updateData.caption !== undefined) {
102
+ file.caption = updateData.caption;
103
+ }
104
+ }
105
+ }
106
+ }));
107
+ },
108
+ /**
109
+ * Process existing files with job tracking for progress updates
110
+ */ async processExistingFiles (jobId, user) {
111
+ const jobService = getService('aiMetadataJobs');
112
+ try {
113
+ // Mark as processing
114
+ await jobService.updateJob(jobId, {
115
+ status: 'processing'
116
+ });
117
+ // Query all images without metadata
118
+ const files = await strapi.db.query('plugin::upload.file').findMany({
119
+ where: {
120
+ mime: {
121
+ $in: SUPPORTED_IMAGE_TYPES
122
+ },
123
+ $or: [
124
+ {
125
+ alternativeText: {
126
+ $null: true
127
+ }
128
+ },
129
+ {
130
+ alternativeText: ''
131
+ },
132
+ {
133
+ caption: {
134
+ $null: true
135
+ }
136
+ },
137
+ {
138
+ caption: ''
139
+ }
140
+ ]
141
+ }
142
+ });
143
+ if (files.length === 0) {
144
+ await jobService.updateJob(jobId, {
145
+ status: 'completed',
146
+ completedAt: new Date()
147
+ });
148
+ return;
149
+ }
150
+ // Process all files at once
151
+ const metadataResults = await this.processFiles(files);
152
+ await this.updateFilesWithAIMetadata(files, metadataResults, user);
153
+ // Mark as completed
154
+ await jobService.updateJob(jobId, {
155
+ status: 'completed',
156
+ completedAt: new Date()
157
+ });
158
+ } catch (error) {
159
+ strapi.log.error('AI metadata job failed', {
160
+ jobId,
161
+ error: error instanceof Error ? error.message : String(error)
162
+ });
163
+ await jobService.updateJob(jobId, {
164
+ status: 'failed',
165
+ completedAt: new Date()
166
+ });
167
+ }
168
+ },
169
+ /**
170
+ * Processes provided files for AI metadata generation
171
+ */ async processFiles (files) {
23
172
  if (!await this.isEnabled() || !aiServerUrl) {
24
173
  throw new Error('AI Metadata service is not enabled');
25
174
  }
@@ -28,27 +177,24 @@ const createAIMetadataService = ({ strapi })=>{
28
177
  const imageFiles = files.map((file, index)=>({
29
178
  file,
30
179
  originalIndex: index
31
- })).filter(({ file })=>file.mimetype?.startsWith('image/'));
180
+ })).filter(({ file })=>file.mime?.startsWith('image/'));
181
+ // Convert filtered image files to InputFile format (uses thumbnails when available)
182
+ const imageInputFiles = imageFiles.map(({ file })=>{
183
+ const thumbnail = file.formats?.thumbnail;
184
+ return {
185
+ filepath: thumbnail?.url || file.url || '',
186
+ mimetype: file.mime,
187
+ originalFilename: file.name,
188
+ size: thumbnail?.size || file.size,
189
+ provider: file.provider
190
+ };
191
+ });
32
192
  // If no image files, return sparse array with all nulls to avoid calling the AI server
33
193
  // This maintains the same array length as input files for proper index alignment
34
194
  if (imageFiles.length === 0) {
35
195
  return new Array(files.length).fill(null);
36
196
  }
37
- const formData = new FormData();
38
- for (const { file } of imageFiles){
39
- const fullUrl = file.provider === 'local' ? strapi.config.get('server.absoluteUrl') + file.filepath : file.filepath;
40
- const resp = await fetch(fullUrl);
41
- if (!resp.ok) {
42
- throw new Error(`Failed to fetch image from URL: ${fullUrl} (${resp.status})`);
43
- }
44
- const ab = await resp.arrayBuffer();
45
- const blob = new Blob([
46
- ab
47
- ], {
48
- type: file.mimetype || undefined
49
- });
50
- formData.append('files', blob);
51
- }
197
+ const formData = await buildFormDataFromFiles(imageInputFiles, strapi.config.get('server.absoluteUrl'), strapi.log);
52
198
  let token;
53
199
  try {
54
200
  const tokenData = await strapi.get('ai').getAiToken();
@@ -58,7 +204,10 @@ const createAIMetadataService = ({ strapi })=>{
58
204
  cause: error instanceof Error ? error : undefined
59
205
  });
60
206
  }
61
- strapi.log.http('Contacting AI Server for media metadata generation');
207
+ strapi.log.http('Contacting AI Server for media metadata generation', {
208
+ aiServerUrl,
209
+ imageCount: imageFiles.length
210
+ });
62
211
  const res = await fetch(`${aiServerUrl}/media-library/generate-metadata`, {
63
212
  method: 'POST',
64
213
  body: formData,
@@ -67,8 +216,9 @@ const createAIMetadataService = ({ strapi })=>{
67
216
  }
68
217
  });
69
218
  if (!res.ok) {
219
+ const errorText = await res.text();
70
220
  throw Error(`AI metadata generation failed`, {
71
- cause: await res.text()
221
+ cause: errorText
72
222
  });
73
223
  }
74
224
  const responseSchema = z.object({
@@ -78,7 +228,7 @@ const createAIMetadataService = ({ strapi })=>{
78
228
  }))
79
229
  });
80
230
  const { results } = responseSchema.parse(await res.json());
81
- strapi.log.http(`Media metadata generated successfully for ${results.length} files`);
231
+ strapi.log.http(`AI generated metadata successfully for ${results.length} files`);
82
232
  // Create sparse array with results at original indices
83
233
  // Example: files=[img1, pdf, img2] -> imageFiles=[{img1, index:0}, {img2, index:2}]
84
234
  // AI results=[meta1, meta2] -> sparse=[meta1, null, meta2]
@@ -1 +1 @@
1
- {"version":3,"file":"ai-metadata.mjs","sources":["../../../server/src/services/ai-metadata.ts"],"sourcesContent":["import type { Core } from '@strapi/types';\nimport { z } from 'zod';\nimport { InputFile } from '../types';\nimport { Settings } from '../controllers/validation/admin/settings';\n\nconst createAIMetadataService = ({ strapi }: { strapi: Core.Strapi }) => {\n const aiServerUrl = process.env.STRAPI_AI_URL || 'https://strapi-ai.apps.strapi.io';\n\n return {\n async isEnabled() {\n // Check if user disabled AI features globally\n const isAIEnabled = strapi.config.get('admin.ai.enabled', true);\n if (!isAIEnabled) {\n return false;\n }\n\n // Check if the user's license grants access to AI features\n const hasAccess = strapi.ee.features.isEnabled('cms-ai');\n if (!hasAccess) {\n return false;\n }\n\n // Check if feature is specifically enabled, defaulting to true\n const settings: Settings = await strapi.plugin('upload').service('upload').getSettings();\n const aiMetadata: boolean = settings.aiMetadata ?? true;\n\n return aiMetadata;\n },\n\n async processFiles(\n files: InputFile[]\n ): Promise<Array<{ altText: string; caption: string } | null>> {\n if (!(await this.isEnabled()) || !aiServerUrl) {\n throw new Error('AI Metadata service is not enabled');\n }\n\n // Filter for image files only and track their original positions\n // We need to maintain the original indices so we can map AI results back correctly\n const imageFiles = files\n .map((file, index) => ({ file, originalIndex: index }))\n .filter(({ file }) => file.mimetype?.startsWith('image/'));\n\n // If no image files, return sparse array with all nulls to avoid calling the AI server\n // This maintains the same array length as input files for proper index alignment\n if (imageFiles.length === 0) {\n return new Array(files.length).fill(null);\n }\n\n const formData = new FormData();\n\n for (const { file } of imageFiles) {\n const fullUrl =\n file.provider === 'local'\n ? strapi.config.get('server.absoluteUrl') + file.filepath\n : file.filepath;\n\n const resp = await fetch(fullUrl);\n if (!resp.ok) {\n throw new Error(`Failed to fetch image from URL: ${fullUrl} (${resp.status})`);\n }\n const ab = await resp.arrayBuffer();\n const blob: Blob = new Blob([ab], { type: file.mimetype || undefined });\n formData.append('files', blob);\n }\n\n let token: string;\n try {\n const tokenData = await strapi.get('ai').getAiToken();\n token = tokenData.token;\n } catch (error) {\n throw new Error('Failed to retrieve AI token', {\n cause: error instanceof Error ? error : undefined,\n });\n }\n\n strapi.log.http('Contacting AI Server for media metadata generation');\n const res = await fetch(`${aiServerUrl}/media-library/generate-metadata`, {\n method: 'POST',\n body: formData,\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!res.ok) {\n throw Error(`AI metadata generation failed`, { cause: await res.text() });\n }\n\n const responseSchema = z.object({\n results: z.array(\n z.object({\n altText: z.string(),\n caption: z.string(),\n })\n ),\n });\n\n const { results } = responseSchema.parse(await res.json());\n strapi.log.http(`Media metadata generated successfully for ${results.length} files`);\n\n // Create sparse array with results at original indices\n // Example: files=[img1, pdf, img2] -> imageFiles=[{img1, index:0}, {img2, index:2}]\n // AI results=[meta1, meta2] -> sparse=[meta1, null, meta2]\n // This ensures metadata[i] corresponds to files[i], with null for non-images\n return imageFiles.reduce((sparseResults, { originalIndex }, resultIndex) => {\n sparseResults[originalIndex] = results[resultIndex];\n return sparseResults;\n }, new Array(files.length).fill(null));\n },\n };\n};\n\nexport { createAIMetadataService };\n"],"names":["createAIMetadataService","strapi","aiServerUrl","process","env","STRAPI_AI_URL","isEnabled","isAIEnabled","config","get","hasAccess","ee","features","settings","plugin","service","getSettings","aiMetadata","processFiles","files","Error","imageFiles","map","file","index","originalIndex","filter","mimetype","startsWith","length","Array","fill","formData","FormData","fullUrl","provider","filepath","resp","fetch","ok","status","ab","arrayBuffer","blob","Blob","type","undefined","append","token","tokenData","getAiToken","error","cause","log","http","res","method","body","headers","Authorization","text","responseSchema","z","object","results","array","altText","string","caption","parse","json","reduce","sparseResults","resultIndex"],"mappings":"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"}
1
+ {"version":3,"file":"ai-metadata.mjs","sources":["../../../server/src/services/ai-metadata.ts"],"sourcesContent":["import type { Core } from '@strapi/types';\nimport { z } from 'zod';\nimport { InputFile, File } from '../types';\nimport { Settings } from '../controllers/validation/admin/settings';\nimport { getService } from '../utils';\nimport { buildFormDataFromFiles } from '../utils/images';\n\n/**\n * Supported image types for AI metadata generation\n * @see https://ai.google.dev/gemini-api/docs/image-understanding\n */\nconst SUPPORTED_IMAGE_TYPES = [\n 'image/png',\n 'image/jpeg',\n 'image/webp',\n 'image/heic',\n 'image/heif',\n] as const;\n\nconst createAIMetadataService = ({ strapi }: { strapi: Core.Strapi }) => {\n const aiServerUrl = process.env.STRAPI_AI_URL || 'https://strapi-ai.apps.strapi.io';\n\n return {\n async isEnabled() {\n // Check if user disabled AI features globally\n const isAIEnabled = strapi.config.get('admin.ai.enabled', true);\n if (!isAIEnabled) {\n return false;\n }\n\n // Check if the user's license grants access to AI features\n const hasAccess = strapi.ee.features.isEnabled('cms-ai');\n if (!hasAccess) {\n return false;\n }\n\n // Check if feature is specifically enabled, defaulting to true\n const settings: Settings = await strapi.plugin('upload').service('upload').getSettings();\n const aiMetadata: boolean = settings.aiMetadata ?? true;\n\n return aiMetadata;\n },\n\n async countImagesWithoutMetadata() {\n const imagesWithoutMetadataCountPromise = strapi.db.query('plugin::upload.file').count({\n where: {\n mime: {\n $in: SUPPORTED_IMAGE_TYPES,\n },\n $or: [\n { alternativeText: { $null: true } },\n { alternativeText: '' },\n { caption: { $null: true } },\n { caption: '' },\n ],\n },\n });\n\n const totalImagesPromise = strapi.db.query('plugin::upload.file').count({\n where: {\n mime: {\n $in: SUPPORTED_IMAGE_TYPES,\n },\n },\n });\n\n const [imagesWithoutMetadataCount, totalImages] = await Promise.all([\n imagesWithoutMetadataCountPromise,\n totalImagesPromise,\n ]);\n\n return { imagesWithoutMetadataCount, totalImages };\n },\n\n /**\n * Update files with AI-generated metadata\n * Shared logic used by both upload flow and retroactive processing\n */\n async updateFilesWithAIMetadata(\n files: File[],\n metadataResults: Array<{ altText: string; caption: string } | null>,\n user: { id: string | number }\n ) {\n const uploadService = strapi.plugin('upload').service('upload');\n\n await Promise.all(\n files.map(async (file, index) => {\n const aiMetadata = metadataResults[index];\n if (aiMetadata) {\n // Only update fields that are missing (null or empty string)\n const updateData: { alternativeText?: string; caption?: string } = {};\n\n if (!file.alternativeText || file.alternativeText === '') {\n updateData.alternativeText = aiMetadata.altText;\n }\n\n if (!file.caption || file.caption === '') {\n updateData.caption = aiMetadata.caption;\n }\n\n // Only update if there are fields to update\n if (Object.keys(updateData).length > 0) {\n await uploadService.updateFileInfo(file.id, updateData, { user });\n\n // Update in-memory file object (needed for upload flow response)\n if (updateData.alternativeText !== undefined) {\n file.alternativeText = updateData.alternativeText;\n }\n if (updateData.caption !== undefined) {\n file.caption = updateData.caption;\n }\n }\n }\n })\n );\n },\n\n /**\n * Process existing files with job tracking for progress updates\n */\n async processExistingFiles(jobId: number, user: { id: string | number }): Promise<void> {\n const jobService = getService('aiMetadataJobs');\n\n try {\n // Mark as processing\n await jobService.updateJob(jobId, { status: 'processing' });\n\n // Query all images without metadata\n const files: File[] = await strapi.db.query('plugin::upload.file').findMany({\n where: {\n mime: {\n $in: SUPPORTED_IMAGE_TYPES,\n },\n $or: [\n { alternativeText: { $null: true } },\n { alternativeText: '' },\n { caption: { $null: true } },\n { caption: '' },\n ],\n },\n });\n\n if (files.length === 0) {\n await jobService.updateJob(jobId, {\n status: 'completed',\n completedAt: new Date(),\n });\n return;\n }\n\n // Process all files at once\n const metadataResults = await this.processFiles(files);\n await this.updateFilesWithAIMetadata(files, metadataResults, user);\n\n // Mark as completed\n await jobService.updateJob(jobId, {\n status: 'completed',\n completedAt: new Date(),\n });\n } catch (error) {\n strapi.log.error('AI metadata job failed', {\n jobId,\n error: error instanceof Error ? error.message : String(error),\n });\n\n await jobService.updateJob(jobId, {\n status: 'failed',\n completedAt: new Date(),\n });\n }\n },\n\n /**\n * Processes provided files for AI metadata generation\n */\n async processFiles(files: File[]): Promise<Array<{ altText: string; caption: string } | null>> {\n if (!(await this.isEnabled()) || !aiServerUrl) {\n throw new Error('AI Metadata service is not enabled');\n }\n\n // Filter for image files only and track their original positions\n // We need to maintain the original indices so we can map AI results back correctly\n const imageFiles = files\n .map((file, index) => ({ file, originalIndex: index }))\n .filter(({ file }) => file.mime?.startsWith('image/'));\n\n // Convert filtered image files to InputFile format (uses thumbnails when available)\n const imageInputFiles = imageFiles.map(({ file }) => {\n const thumbnail = (file.formats as any)?.thumbnail;\n return {\n filepath: thumbnail?.url || file.url || '',\n mimetype: file.mime,\n originalFilename: file.name,\n size: thumbnail?.size || file.size,\n provider: file.provider,\n } as InputFile;\n });\n\n // If no image files, return sparse array with all nulls to avoid calling the AI server\n // This maintains the same array length as input files for proper index alignment\n if (imageFiles.length === 0) {\n return new Array(files.length).fill(null);\n }\n\n const formData = await buildFormDataFromFiles(\n imageInputFiles,\n strapi.config.get('server.absoluteUrl'),\n strapi.log\n );\n\n let token: string;\n try {\n const tokenData = await strapi.get('ai').getAiToken();\n token = tokenData.token;\n } catch (error) {\n throw new Error('Failed to retrieve AI token', {\n cause: error instanceof Error ? error : undefined,\n });\n }\n\n strapi.log.http('Contacting AI Server for media metadata generation', {\n aiServerUrl,\n imageCount: imageFiles.length,\n });\n\n const res = await fetch(`${aiServerUrl}/media-library/generate-metadata`, {\n method: 'POST',\n body: formData,\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!res.ok) {\n const errorText = await res.text();\n throw Error(`AI metadata generation failed`, { cause: errorText });\n }\n\n const responseSchema = z.object({\n results: z.array(\n z.object({\n altText: z.string(),\n caption: z.string(),\n })\n ),\n });\n\n const { results } = responseSchema.parse(await res.json());\n strapi.log.http(`AI generated metadata successfully for ${results.length} files`);\n\n // Create sparse array with results at original indices\n // Example: files=[img1, pdf, img2] -> imageFiles=[{img1, index:0}, {img2, index:2}]\n // AI results=[meta1, meta2] -> sparse=[meta1, null, meta2]\n // This ensures metadata[i] corresponds to files[i], with null for non-images\n return imageFiles.reduce((sparseResults, { originalIndex }, resultIndex) => {\n sparseResults[originalIndex] = results[resultIndex];\n return sparseResults;\n }, new Array(files.length).fill(null));\n },\n };\n};\n\nexport { createAIMetadataService };\n"],"names":["SUPPORTED_IMAGE_TYPES","createAIMetadataService","strapi","aiServerUrl","process","env","STRAPI_AI_URL","isEnabled","isAIEnabled","config","get","hasAccess","ee","features","settings","plugin","service","getSettings","aiMetadata","countImagesWithoutMetadata","imagesWithoutMetadataCountPromise","db","query","count","where","mime","$in","$or","alternativeText","$null","caption","totalImagesPromise","imagesWithoutMetadataCount","totalImages","Promise","all","updateFilesWithAIMetadata","files","metadataResults","user","uploadService","map","file","index","updateData","altText","Object","keys","length","updateFileInfo","id","undefined","processExistingFiles","jobId","jobService","getService","updateJob","status","findMany","completedAt","Date","processFiles","error","log","Error","message","String","imageFiles","originalIndex","filter","startsWith","imageInputFiles","thumbnail","formats","filepath","url","mimetype","originalFilename","name","size","provider","Array","fill","formData","buildFormDataFromFiles","token","tokenData","getAiToken","cause","http","imageCount","res","fetch","method","body","headers","Authorization","ok","errorText","text","responseSchema","z","object","results","array","string","parse","json","reduce","sparseResults","resultIndex"],"mappings":"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"}
@@ -10,6 +10,7 @@ var metrics = require('./metrics.js');
10
10
  var apiUploadFolder = require('./api-upload-folder.js');
11
11
  var index = require('./extensions/index.js');
12
12
  var aiMetadata = require('./ai-metadata.js');
13
+ var aiMetadataJobs = require('./ai-metadata-jobs.js');
13
14
 
14
15
  const services = {
15
16
  provider,
@@ -21,7 +22,8 @@ const services = {
21
22
  'image-manipulation': imageManipulation,
22
23
  'api-upload-folder': apiUploadFolder,
23
24
  extensions: index,
24
- aiMetadata: aiMetadata.createAIMetadataService
25
+ aiMetadata: aiMetadata.createAIMetadataService,
26
+ aiMetadataJobs: aiMetadataJobs.createAIMetadataJobsService
25
27
  };
26
28
 
27
29
  exports.services = services;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../server/src/services/index.ts"],"sourcesContent":["import provider from './provider';\nimport upload from './upload';\nimport imageManipulation from './image-manipulation';\nimport folder from './folder';\nimport file from './file';\nimport weeklyMetrics from './weekly-metrics';\nimport metrics from './metrics';\nimport apiUploadFolder from './api-upload-folder';\nimport extensions from './extensions';\nimport { createAIMetadataService } from './ai-metadata';\n\nexport const services = {\n provider,\n upload,\n folder,\n file,\n weeklyMetrics,\n metrics,\n 'image-manipulation': imageManipulation,\n 'api-upload-folder': apiUploadFolder,\n extensions,\n aiMetadata: createAIMetadataService,\n};\n"],"names":["services","provider","upload","folder","file","weeklyMetrics","metrics","imageManipulation","apiUploadFolder","extensions","aiMetadata","createAIMetadataService"],"mappings":";;;;;;;;;;;;;MAWaA,QAAW,GAAA;AACtBC,IAAAA,QAAAA;AACAC,IAAAA,MAAAA;AACAC,IAAAA,MAAAA;AACAC,IAAAA,IAAAA;AACAC,IAAAA,aAAAA;AACAC,IAAAA,OAAAA;IACA,oBAAsBC,EAAAA,iBAAAA;IACtB,mBAAqBC,EAAAA,eAAAA;AACrBC,gBAAAA,KAAAA;IACAC,UAAYC,EAAAA;AACd;;;;"}
1
+ {"version":3,"file":"index.js","sources":["../../../server/src/services/index.ts"],"sourcesContent":["import provider from './provider';\nimport upload from './upload';\nimport imageManipulation from './image-manipulation';\nimport folder from './folder';\nimport file from './file';\nimport weeklyMetrics from './weekly-metrics';\nimport metrics from './metrics';\nimport apiUploadFolder from './api-upload-folder';\nimport extensions from './extensions';\nimport { createAIMetadataService } from './ai-metadata';\nimport { createAIMetadataJobsService } from './ai-metadata-jobs';\n\nexport const services = {\n provider,\n upload,\n folder,\n file,\n weeklyMetrics,\n metrics,\n 'image-manipulation': imageManipulation,\n 'api-upload-folder': apiUploadFolder,\n extensions,\n aiMetadata: createAIMetadataService,\n aiMetadataJobs: createAIMetadataJobsService,\n};\n"],"names":["services","provider","upload","folder","file","weeklyMetrics","metrics","imageManipulation","apiUploadFolder","extensions","aiMetadata","createAIMetadataService","aiMetadataJobs","createAIMetadataJobsService"],"mappings":";;;;;;;;;;;;;;MAYaA,QAAW,GAAA;AACtBC,IAAAA,QAAAA;AACAC,IAAAA,MAAAA;AACAC,IAAAA,MAAAA;AACAC,IAAAA,IAAAA;AACAC,IAAAA,aAAAA;AACAC,IAAAA,OAAAA;IACA,oBAAsBC,EAAAA,iBAAAA;IACtB,mBAAqBC,EAAAA,eAAAA;AACrBC,gBAAAA,KAAAA;IACAC,UAAYC,EAAAA,kCAAAA;IACZC,cAAgBC,EAAAA;AAClB;;;;"}
@@ -8,6 +8,7 @@ import metrics from './metrics.mjs';
8
8
  import apiUploadFolder from './api-upload-folder.mjs';
9
9
  import extensions from './extensions/index.mjs';
10
10
  import { createAIMetadataService } from './ai-metadata.mjs';
11
+ import { createAIMetadataJobsService } from './ai-metadata-jobs.mjs';
11
12
 
12
13
  const services = {
13
14
  provider,
@@ -19,7 +20,8 @@ const services = {
19
20
  'image-manipulation': imageManipulation,
20
21
  'api-upload-folder': apiUploadFolder,
21
22
  extensions,
22
- aiMetadata: createAIMetadataService
23
+ aiMetadata: createAIMetadataService,
24
+ aiMetadataJobs: createAIMetadataJobsService
23
25
  };
24
26
 
25
27
  export { services };