{"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      if (strapi.ai.admin.isEnabled() === false) {\n        return false;\n      }\n      const settings: Settings = await strapi.plugin('upload').service('upload').getSettings();\n      return settings.aiMetadata ?? true;\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.ai.admin.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","ai","admin","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","config","get","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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"}