Реализация автоматической генерации alt-текстов для изображений товаров (AI)
Alt-текст у изображения выполняет две функции: помогает поисковику понять содержание страницы и делает сайт доступным для незрячих пользователей. В большинстве интернет-магазинов alt-теги либо пустые, либо содержат только название файла вроде IMG_4821.jpg. Это потерянный SEO-сигнал и нарушение доступности.
При наличии тысяч товарных изображений задача решается автоматически — либо на основе данных о товаре, либо через мультимодальный анализ самого изображения.
Два подхода
Подход 1: генерация по метаданным товара — быстро, дёшево, не требует загрузки изображений в модель. Подходит, когда фотографии стандартные (белый фон, один ракурс).
Подход 2: мультимодальный анализ изображения — модель видит само фото и описывает, что на нём. Нужен для lifestyle-фотографий, изображений с несколькими объектами или когда важны визуальные детали.
Подход 1: по метаданным
function buildAltFromMetadata(
product: Product,
imageIndex: number,
imageType: "main" | "detail" | "lifestyle"
): string {
const base = `${product.brand} ${product.name}`;
if (imageType === "main") {
return `${base} — фото ${imageIndex + 1}`;
}
if (imageType === "detail") {
const feature = product.attributes.detailFeatures?.[imageIndex] ?? "деталь";
return `${base}: ${feature}`;
}
return `${base} в использовании`;
}
Простой, детерминированный вариант. Не требует API-вызовов, работает мгновенно при импорте товаров.
Подход 2: мультимодальный (GPT-4o Vision)
import { openai } from "../lib/openai";
async function generateAltFromImage(
imageUrl: string,
product: Product
): Promise<string> {
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "user",
content: [
{
type: "image_url",
image_url: { url: imageUrl, detail: "low" }, // low — дешевле, достаточно для alt
},
{
type: "text",
text: `Write a concise alt text for this product image in Russian.
Product: ${product.name}, brand: ${product.brand}, category: ${product.category}.
Rules:
- 60–120 characters
- Describe what is visible in the image, not the product in general
- Start with product name only if it's clearly identifiable in the photo
- No "фото", "изображение", "картинка" at the start
- No markdown, just plain text`,
},
],
},
],
max_tokens: 80,
temperature: 0.3,
});
return response.choices[0].message.content?.trim() ?? "";
}
detail: "low" снижает стоимость с ~$0.002 до ~$0.0003 за изображение — достаточно для генерации alt, не нужно высокое разрешение.
Пакетная обработка изображений
import { Queue, Worker } from "bullmq";
const altQueue = new Queue("alt-generation");
export async function queueImagesForAltGeneration(
productIds: string[]
) {
const products = await db.products.findMany({
where: { id: { in: productIds } },
include: { images: true },
});
const jobs = products.flatMap((product) =>
product.images
.filter((img) => !img.altText || img.altText === "")
.map((img) => ({
name: "generate-alt",
data: { imageId: img.id, productId: product.id, imageUrl: img.url },
}))
);
await altQueue.addBulk(jobs);
}
const altWorker = new Worker(
"alt-generation",
async (job) => {
const { imageId, productId, imageUrl } = job.data;
const product = await db.products.findById(productId);
// Определяем, нужен ли мультимодальный анализ
const useVision = product.imageType === "lifestyle" || product.useVisionForAlt;
let altText: string;
if (useVision) {
altText = await generateAltFromImage(imageUrl, product);
} else {
const imageIndex = product.images.findIndex((i: any) => i.id === imageId);
altText = buildAltFromMetadata(product, imageIndex, "main");
}
await db.productImages.update({
where: { id: imageId },
data: { altText, altGeneratedAt: new Date() },
});
},
{ connection: redisConnection, concurrency: 20 }
);
Встройка в CMS
После генерации alt-текст сохраняется в таблице изображений и рендерится в шаблоне:
<img
src="{{ image.url }}"
alt="{{ image.altText }}"
loading="lazy"
width="{{ image.width }}"
height="{{ image.height }}"
/>
Если alt пустой (генерация ещё не прошла или провалилась) — фолбэк на название товара, но не пустая строка:
const alt = image.altText || `${product.brand} ${product.name}`;
Аудит существующего каталога
Перед запуском генерации полезно понять масштаб проблемы:
SELECT
COUNT(*) FILTER (WHERE alt_text IS NULL OR alt_text = '') AS missing_alt,
COUNT(*) FILTER (WHERE alt_text = file_name) AS filename_as_alt,
COUNT(*) total
FROM product_images;
Это покажет, сколько изображений нуждаются в обработке и позволит спланировать нагрузку на API. 50 000 изображений при мультимодальной генерации — около $15, при генерации по метаданным — бесплатно.







