Bulk Image Alt Text Generator - AI Captions from Datasets
Pricing
from $10.00 / 1,000 alt text generateds
Bulk Image Alt Text Generator - AI Captions from Datasets
Generate SEO and accessibility alt text for thousands of product images from image URLs or a Dataset. SKU and row kept, 36 languages, length limit, CSV-ready columns. Pay per image.
Pricing
from $10.00 / 1,000 alt text generateds
Rating
0.0
(0)
Developer
Vadim Bezrukov
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Write alt text for a whole product catalog in one run. Paste image URLs, or point the Actor at the Dataset of a scraper or of Parallel Bulk File Downloader, and every image comes back as one row with your SKU, the image URL and a short, factual alt text that is ready for your store's bulk import. Up to 10,000 images per run, in 36 languages.
The prefilled input describes three openly licensed product photos from Wikimedia Commons: an antique desk chair, a mug and a bottle of olive oil. It finishes in under a minute. You should see three rows with status SUCCESS and an altText such as "Clear glass bottle filled with golden olive oil and sealed with a cork, beside a small glass bowl." Open the Alt text view of the Dataset to see them as a table next to the images.
Price: $0.01 per image that gets alt text, platform usage included. The prefill costs about $0.03, and 1,000 images cost $10. Repeated URLs, failed images and retries are free. Update history is in CHANGELOG.md.
Alt text for a Dataset of product images
The usual chain: a scraper, or Parallel Bulk File Downloader, leaves a Dataset with one image URL per row. Point this Actor at it and keep each row's SKU:
{"datasetId": "YOUR_DATASET_ID","imageField": "source_url","idField": "metadata.sku","contextFields": ["title", "brand"],"language": "en","altTextMaxLength": 125}
imageField is the column with the image URL: source_url after Parallel Bulk File Downloader, url or imageUrl after most scrapers, images.0 for the first entry of a list. Dot paths reach nested fields. idField is copied to sourceId on each row; after the Downloader the SKU sits in metadata.sku. Rows without it use their Dataset row number.
contextFields (up to 10) pass product fields such as the title, brand or color to the model as context. It uses them when the image agrees, so "Linen shirt, Acme" in your data becomes part of the alt text, and it never adds a brand that is neither in your data nor printed clearly on the product.
The run reads up to maxImages rows (default 1,000, maximum 10,000) from datasetOffset. When the Dataset has more rows, RUN_SUMMARY gives nextDatasetOffset and its nextAction names the offset for the next run. A row without an image URL gets INVALID_INPUT and the other rows continue.
To run it every time the scraper or Downloader finishes, open that Actor in Apify Console, go to Integrations, connect this Actor and use "datasetId": "{{resource.defaultDatasetId}}" in the input. Under limited permissions this Actor can read only the Dataset named in datasetId; a Dataset ID that sits only inside the integration's payload is not read, and the run stops with an INVALID_INPUT summary that says how to fix it.
Import the alt text into your store
Each row has sourceId (your SKU or id), imageUrl and altText. Download them as CSV:
https://api.apify.com/v2/datasets/<datasetId>/items?format=csv&fields=sourceId,imageUrl,altText,status
Keep the rows whose status is SUCCESS, match sourceId to your SKU or handle column and imageUrl to the image, and put altText in the image alt column (in a Shopify product CSV it is called Image Alt Text). A failed row has an empty altText, and its status says why.
Language, length and captions
language: one of 36 languages, English by default. The model writes in that language; context fields can stay in any language.altTextMaxLength: 40-250 characters, 125 by default, which suits most screen readers and store fields. The model aims a little below the limit. A longer answer is cut at the last sentence or clause, never in the middle of a word, andRUN_SUMMARY.shortenedAltTextscounts those.includeCaption: adds acaptionof one to three sentences with the details that did not fit, at the same price.
The alt text describes what is visible: product type, color, material, pattern, shape, and how it is shown (worn by a model, on a table, in a room). It does not start with "image of", it names no real person and guesses nothing about anyone's identity, age or ethnicity, and it reads a brand only from your data or from large, legible print on the product.
Output
One row per input image, in the order images finish; sort by inputIndex for input order. Field names are camelCase and schemaVersion is 1; fields are only ever added. The values below are illustrative.
{"inputIndex": 12,"sourceId": "SHIRT-LINEN-BLUE-M","imageUrl": "https://cdn.example-shop.com/p/shirt-blue.jpg","status": "SUCCESS","altText": "Light blue linen shirt with a button-down collar and chest pocket, laid flat on white.","caption": null,"language": "en","chars": 87,"model": "gpt-6-luna","imageSha256": "9f2c...","charged": true,"duplicateOf": null,"errorCode": null,"error": null,"scrapedAt": "2026-10-23T12:00:00Z","schemaVersion": 1}
When the same URL appears more than once, it is downloaded and described once. The first row carries the charge (charged: true); the others get the same alt text for free and point at it with duplicateOf. imageSha256 is the hash of the downloaded image, so a later run can tell that an image did not change.
Dataset views:
- Alt text:
sourceId, the image,status,altText,caption,languageandchars. - Status and errors:
sourceId, the image URL,status,errorCodeanderror. - All rows: every row with its charge and duplicate details.
Views choose columns, not rows: every view lists all rows, so filter on status for the successful ones.
RUN_SUMMARY in the Key-Value Store holds the overall status (SUCCESS, PARTIAL, ALL_FAILED, NO_RESULTS or FAILED), counts per row status, charged images, Dataset paging and nextAction. Read it first. When some images can be finished by another run (time limit, charge limit, a model or network error), RETRY_INPUT holds them with their ids and product fields: paste it as the JSON input of a new run. Successful images are not in it, so they are never paid for twice.
Pricing
| Event | When it is charged | Price |
|---|---|---|
apify-actor-start | Actor start, once per GB of memory (minimum one) | $0.00005 |
alt-text-generated | one unique image that got a valid alt text, charged when its row is stored | $0.01 |
Examples: the prefill (3 images) is about $0.03. 1,000 images cost $10, and a Dataset of 5,000 product photos costs $50. Platform usage is included in the price. Console runs start with a $10 limit from the default run options; raise maxTotalChargeUsd for a bigger batch, and pass your own limit on API starts.
Failed downloads, files that are not images, refusals, model errors, duplicate URLs, retries and images stopped by the run timeout are free. Before each model call the run reserves one event inside maxTotalChargeUsd; when the limit cannot pay for another image, the remaining images get BUDGET_EXCEEDED, uncharged, and go to RETRY_INPUT. Rows that already succeeded stay in the Dataset.
Limits
- 1-10,000 input rows per run (
maxImages), 1-20 images at a time (concurrency, default 8), and at most 4 downloads at once from the same host so a shop's image server is not flooded - JPEG, PNG, WebP, GIF (first frame), AVIF, TIFF and BMP; SVG is not supported
- 30 MB per file, 60 megapixels per image, at least 16 pixels on each side
- public
httpandhttpsURLs on ports 80 and 443; a protocol-relative//cdn.example.com/a.jpg, common in shop exports, is read ashttps - the model sees each image at most 512 pixels on its longest side; transparent areas are shown on white
- memory 1024 MB by default; run timeout 1 hour by default. About 2-4 seconds per image at concurrency 8, so 1,000 images take roughly 5-10 minutes
Near the run timeout, images that cannot start in time get RUN_TIME_LIMIT, uncharged, and the run still finishes with its summary and RETRY_INPUT.
API, n8n, Make and MCP
Start the Actor with the Apify run API and one of the JSON inputs above. In n8n use the Apify node, or an HTTP Request node that POSTs to https://api.apify.com/v2/acts/automa-flow~bulk-image-alt-text-generator/run-sync-get-dataset-items?view=altText with your Apify token in the Authorization header: it waits for the run and returns one row per image; keep those whose status is SUCCESS. In Make, use the Apify app's "Run an Actor" module with "wait for finish", then "Get Dataset Items" with view altText.
API callers can send images with their own ids and product fields in images:
{"images": [{"url": "https://cdn.example-shop.com/p/1.jpg", "id": "SKU-1", "context": {"title": "Oak side table"}},{"url": "https://cdn.example-shop.com/p/2.jpg", "id": "SKU-2"}],"language": "de"}
Ask an agent: "Write English alt text for these product image URLs and keep my SKUs." The direct MCP endpoint is https://mcp.apify.com?tools=automa-flow/bulk-image-alt-text-generator. An agent should pass image file URLs or a Dataset id, bound the call with maxImages and maxTotalChargeUsd, read RUN_SUMMARY first, and on PARTIAL or ALL_FAILED resend only RETRY_INPUT. Execution uses the caller's Apify account.
A second run usually means new products: schedule the Actor over the Dataset your scraper refreshes, or connect it as an integration so each new batch of product images gets alt text without manual steps.
Failure statuses
| Status | Meaning |
|---|---|
SUCCESS | Alt text written and stored |
INVALID_INPUT | No image URL in the row, an unsupported scheme, or a private or local address |
IMAGE_FETCH_FAILED | HTTP 404, 403 or another HTTP error, DNS or network failure, download timeout |
NOT_AN_IMAGE | The URL returned a web page, an SVG or another file that is not a readable image, or a damaged image |
IMAGE_TOO_LARGE | Over 30 MB or 60 megapixels |
IMAGE_TOO_SMALL | Under 16 pixels on a side, usually a placeholder or tracking pixel |
MODEL_REFUSED | The model declined to describe the image |
MODEL_FAILED | The model returned no usable alt text or was unavailable |
BUDGET_EXCEEDED | maxTotalChargeUsd could not pay for another image |
RUN_TIME_LIMIT | The run timeout came before the image finished |
Only SUCCESS rows have alt text and only they are charged. The run finishes successfully when per-image failures are the only problem. It fails the whole run when the model service cannot be used at all (for example it rejects the Actor's key or is down for every image), when the run timeout left no image started, or when the Dataset cannot be read or written; rows that were already stored stay in the Dataset.
If a run fails or looks wrong, open an Issue and include the run URL. Apify shares that run with us automatically, so there is no need to paste your input.
Responsible use and privacy
Process only images you have the rights or permission to use. You need the rights or permission to process the files you submit.
Each image is downloaded by this Actor, reduced to at most 512 pixels as a JPEG and sent, with the product fields you selected, to OpenAI's API (model gpt-6-luna) to write the alt text. Requests are sent with response storage turned off (store: false). OpenAI's API terms apply to that processing. This Actor does not keep the images: each downloaded file is deleted once it is described. The run log shows image hosts and paths with query values redacted, and counts; it never shows alt texts. The prompt tells the model to describe people only by what they wear or do, and not to identify anyone.
Image URLs are fetched with a normal HTTP client that allows http and https on ports 80 and 443, rejects embedded passwords, localhost, private, reserved and metadata addresses, checks DNS before the request and on every redirect, and stops at the byte cap. It sends no cookies and no Authorization header to the image host.
Technical details
Python 3.12 on the Apify Python image, Pillow for decoding and resizing, and one OpenAI Responses API call per unique image with a strict JSON schema and reasoning turned off. Model calls are retried up to three times on rate limits, server errors and broken connections, with backoff; refusals, bad requests and timeouts are not retried. Ten model outages in a row stop the batch instead of hammering a broken service. A restarted run (for example after a platform migration) skips every row already in the Dataset and never charges an image twice.