@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.
- package/dist/admin/components/EditAssetDialog/EditAssetContent.js +32 -3
- package/dist/admin/components/EditAssetDialog/EditAssetContent.js.map +1 -1
- package/dist/admin/components/EditAssetDialog/EditAssetContent.mjs +32 -3
- package/dist/admin/components/EditAssetDialog/EditAssetContent.mjs.map +1 -1
- package/dist/admin/components/EditAssetDialog/PreviewBox/AssetPreview.js.map +1 -1
- package/dist/admin/components/EditAssetDialog/PreviewBox/AssetPreview.mjs.map +1 -1
- package/dist/admin/components/EditAssetDialog/PreviewBox/FocalPointActions.js +57 -0
- package/dist/admin/components/EditAssetDialog/PreviewBox/FocalPointActions.js.map +1 -0
- package/dist/admin/components/EditAssetDialog/PreviewBox/FocalPointActions.mjs +55 -0
- package/dist/admin/components/EditAssetDialog/PreviewBox/FocalPointActions.mjs.map +1 -0
- package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewBox.js +96 -20
- package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewBox.js.map +1 -1
- package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewBox.mjs +98 -22
- package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewBox.mjs.map +1 -1
- package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewComponents.js +47 -0
- package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewComponents.js.map +1 -1
- package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewComponents.mjs +44 -1
- package/dist/admin/components/EditAssetDialog/PreviewBox/PreviewComponents.mjs.map +1 -1
- package/dist/admin/future/App.js +45 -0
- package/dist/admin/future/App.js.map +1 -0
- package/dist/admin/future/App.mjs +43 -0
- package/dist/admin/future/App.mjs.map +1 -0
- package/dist/admin/future/pages/AIGenerationPage.js +24 -0
- package/dist/admin/future/pages/AIGenerationPage.js.map +1 -0
- package/dist/admin/future/pages/AIGenerationPage.mjs +22 -0
- package/dist/admin/future/pages/AIGenerationPage.mjs.map +1 -0
- package/dist/admin/future/pages/MediaLibraryPage.js +119 -0
- package/dist/admin/future/pages/MediaLibraryPage.js.map +1 -0
- package/dist/admin/future/pages/MediaLibraryPage.mjs +98 -0
- package/dist/admin/future/pages/MediaLibraryPage.mjs.map +1 -0
- package/dist/admin/future/services/api.js +28 -0
- package/dist/admin/future/services/api.js.map +1 -0
- package/dist/admin/future/services/api.mjs +25 -0
- package/dist/admin/future/services/api.mjs.map +1 -0
- package/dist/admin/future/utils/translations.js +8 -0
- package/dist/admin/future/utils/translations.js.map +1 -0
- package/dist/admin/future/utils/translations.mjs +6 -0
- package/dist/admin/future/utils/translations.mjs.map +1 -0
- package/dist/admin/hooks/useAIMetadataJob.js +114 -0
- package/dist/admin/hooks/useAIMetadataJob.js.map +1 -0
- package/dist/admin/hooks/useAIMetadataJob.mjs +93 -0
- package/dist/admin/hooks/useAIMetadataJob.mjs.map +1 -0
- package/dist/admin/hooks/useEditAsset.js +1 -0
- package/dist/admin/hooks/useEditAsset.js.map +1 -1
- package/dist/admin/hooks/useEditAsset.mjs +1 -0
- package/dist/admin/hooks/useEditAsset.mjs.map +1 -1
- package/dist/admin/index.js +23 -4
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +24 -5
- package/dist/admin/index.mjs.map +1 -1
- package/dist/admin/package.json.js +6 -5
- package/dist/admin/package.json.js.map +1 -1
- package/dist/admin/package.json.mjs +6 -5
- package/dist/admin/package.json.mjs.map +1 -1
- package/dist/admin/pages/App/ConfigureTheView/ConfigureTheView.js +1 -0
- package/dist/admin/pages/App/ConfigureTheView/ConfigureTheView.js.map +1 -1
- package/dist/admin/pages/App/ConfigureTheView/ConfigureTheView.mjs +1 -0
- package/dist/admin/pages/App/ConfigureTheView/ConfigureTheView.mjs.map +1 -1
- package/dist/admin/pages/App/components/Header.js +3 -0
- package/dist/admin/pages/App/components/Header.js.map +1 -1
- package/dist/admin/pages/App/components/Header.mjs +3 -0
- package/dist/admin/pages/App/components/Header.mjs.map +1 -1
- package/dist/admin/pages/SettingsPage/SettingsPage.js +252 -67
- package/dist/admin/pages/SettingsPage/SettingsPage.js.map +1 -1
- package/dist/admin/pages/SettingsPage/SettingsPage.mjs +256 -71
- package/dist/admin/pages/SettingsPage/SettingsPage.mjs.map +1 -1
- package/dist/admin/src/components/EditAssetDialog/PreviewBox/AssetPreview.d.ts +1 -2
- package/dist/admin/src/components/EditAssetDialog/PreviewBox/FocalPointActions.d.ts +7 -0
- package/dist/admin/src/components/EditAssetDialog/PreviewBox/PreviewBox.d.ts +6 -2
- package/dist/admin/src/components/EditAssetDialog/PreviewBox/PreviewComponents.d.ts +13 -0
- package/dist/admin/src/future/App.d.ts +1 -0
- package/dist/admin/src/future/pages/AIGenerationPage.d.ts +1 -0
- package/dist/admin/src/future/pages/MediaLibraryPage.d.ts +1 -0
- package/dist/admin/src/future/services/api.d.ts +6 -0
- package/dist/admin/src/future/services/settings.d.ts +2 -0
- package/dist/admin/src/future/utils/translations.d.ts +1 -0
- package/dist/admin/src/hooks/useAIMetadataJob.d.ts +9 -0
- package/dist/admin/translations/de.json.js +44 -1
- package/dist/admin/translations/de.json.js.map +1 -1
- package/dist/admin/translations/de.json.mjs +44 -1
- package/dist/admin/translations/de.json.mjs.map +1 -1
- package/dist/admin/translations/en.json.js +17 -0
- package/dist/admin/translations/en.json.js.map +1 -1
- package/dist/admin/translations/en.json.mjs +17 -0
- package/dist/admin/translations/en.json.mjs.map +1 -1
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/server/bootstrap.mjs +1 -0
- package/dist/server/bootstrap.mjs.map +1 -1
- package/dist/server/content-types/file.js +4 -0
- package/dist/server/content-types/file.js.map +1 -1
- package/dist/server/content-types/file.mjs +4 -0
- package/dist/server/content-types/file.mjs.map +1 -1
- package/dist/server/controllers/admin-file.js +86 -0
- package/dist/server/controllers/admin-file.js.map +1 -1
- package/dist/server/controllers/admin-file.mjs +86 -0
- package/dist/server/controllers/admin-file.mjs.map +1 -1
- package/dist/server/controllers/admin-upload.js +3 -23
- package/dist/server/controllers/admin-upload.js.map +1 -1
- package/dist/server/controllers/admin-upload.mjs +3 -23
- package/dist/server/controllers/admin-upload.mjs.map +1 -1
- package/dist/server/controllers/validation/admin/upload.js +5 -0
- package/dist/server/controllers/validation/admin/upload.js.map +1 -1
- package/dist/server/controllers/validation/admin/upload.mjs +5 -0
- package/dist/server/controllers/validation/admin/upload.mjs.map +1 -1
- package/dist/server/controllers/validation/content-api/upload.js +6 -1
- package/dist/server/controllers/validation/content-api/upload.js.map +1 -1
- package/dist/server/controllers/validation/content-api/upload.mjs +6 -1
- package/dist/server/controllers/validation/content-api/upload.mjs.map +1 -1
- package/dist/server/models/ai-metadata-job.js +36 -0
- package/dist/server/models/ai-metadata-job.js.map +1 -0
- package/dist/server/models/ai-metadata-job.mjs +33 -0
- package/dist/server/models/ai-metadata-job.mjs.map +1 -0
- package/dist/server/register.js +3 -0
- package/dist/server/register.js.map +1 -1
- package/dist/server/register.mjs +3 -0
- package/dist/server/register.mjs.map +1 -1
- package/dist/server/routes/admin.js +46 -0
- package/dist/server/routes/admin.js.map +1 -1
- package/dist/server/routes/admin.mjs +46 -0
- package/dist/server/routes/admin.mjs.map +1 -1
- package/dist/server/services/ai-metadata-jobs.js +72 -0
- package/dist/server/services/ai-metadata-jobs.js.map +1 -0
- package/dist/server/services/ai-metadata-jobs.mjs +70 -0
- package/dist/server/services/ai-metadata-jobs.mjs.map +1 -0
- package/dist/server/services/ai-metadata.js +170 -20
- package/dist/server/services/ai-metadata.js.map +1 -1
- package/dist/server/services/ai-metadata.mjs +170 -20
- package/dist/server/services/ai-metadata.mjs.map +1 -1
- package/dist/server/services/index.js +3 -1
- package/dist/server/services/index.js.map +1 -1
- package/dist/server/services/index.mjs +3 -1
- package/dist/server/services/index.mjs.map +1 -1
- package/dist/server/services/upload.js +3 -1
- package/dist/server/services/upload.js.map +1 -1
- package/dist/server/services/upload.mjs +3 -1
- package/dist/server/services/upload.mjs.map +1 -1
- package/dist/server/src/bootstrap.d.ts.map +1 -1
- package/dist/server/src/content-types/file.d.ts +4 -0
- package/dist/server/src/content-types/file.d.ts.map +1 -1
- package/dist/server/src/content-types/index.d.ts +4 -0
- package/dist/server/src/content-types/index.d.ts.map +1 -1
- package/dist/server/src/controllers/admin-file.d.ts +3 -0
- package/dist/server/src/controllers/admin-file.d.ts.map +1 -1
- package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
- package/dist/server/src/controllers/index.d.ts +3 -0
- package/dist/server/src/controllers/index.d.ts.map +1 -1
- package/dist/server/src/controllers/validation/admin/upload.d.ts +240 -0
- package/dist/server/src/controllers/validation/admin/upload.d.ts.map +1 -1
- package/dist/server/src/controllers/validation/content-api/upload.d.ts +180 -0
- package/dist/server/src/controllers/validation/content-api/upload.d.ts.map +1 -1
- package/dist/server/src/index.d.ts +32 -2
- package/dist/server/src/index.d.ts.map +1 -1
- package/dist/server/src/models/ai-metadata-job.d.ts +5 -0
- package/dist/server/src/models/ai-metadata-job.d.ts.map +1 -0
- package/dist/server/src/models/index.d.ts +5 -0
- package/dist/server/src/models/index.d.ts.map +1 -0
- package/dist/server/src/register.d.ts.map +1 -1
- package/dist/server/src/routes/admin.d.ts.map +1 -1
- package/dist/server/src/services/ai-metadata-jobs.d.ts +14 -0
- package/dist/server/src/services/ai-metadata-jobs.d.ts.map +1 -0
- package/dist/server/src/services/ai-metadata.d.ts +25 -2
- package/dist/server/src/services/ai-metadata.d.ts.map +1 -1
- package/dist/server/src/services/index.d.ts +25 -2
- package/dist/server/src/services/index.d.ts.map +1 -1
- package/dist/server/src/services/upload.d.ts +1 -1
- package/dist/server/src/services/upload.d.ts.map +1 -1
- package/dist/server/src/types.d.ts +6 -0
- package/dist/server/src/types.d.ts.map +1 -1
- package/dist/server/src/utils/images.d.ts +7 -0
- package/dist/server/src/utils/images.d.ts.map +1 -0
- package/dist/server/src/utils/index.d.ts +2 -0
- package/dist/server/src/utils/index.d.ts.map +1 -1
- package/dist/server/utils/images.js +35 -0
- package/dist/server/utils/images.js.map +1 -0
- package/dist/server/utils/images.mjs +33 -0
- package/dist/server/utils/images.mjs.map +1 -0
- package/dist/server/utils/index.js.map +1 -1
- package/dist/server/utils/index.mjs.map +1 -1
- package/dist/shared/contracts/ai-metadata-jobs.d.ts +53 -0
- package/dist/shared/contracts/ai-metadata-jobs.d.ts.map +1 -0
- package/dist/shared/contracts/files.d.ts +39 -0
- 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
|
|
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.
|
|
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 =
|
|
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:
|
|
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(`
|
|
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":"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"}
|
|
@@ -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
|
|
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.
|
|
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 =
|
|
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:
|
|
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(`
|
|
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":"
|
|
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 };
|