Finally Accessible: Efficiently Populate ALT Texts with AI

Antonio Blago
Antonio Blago

Finally Accessible: Efficiently Filling ALT Texts with AI   Minutes of reading time remaining By Antonio Blago September 14, 2025

Python, SEO, Tutorial

Practical Example in Shopify

In my job as an SEO freelancer for larger e-commerce shops like PURELEI, I was faced with the task of filling almost 40,000 ALT texts for images after a relaunch. So I tested dozens of Shopify apps—but none of them could reliably handle the work at this scale and to my quality standards. Shouldn't this have been solved by 2025?

It's just not that simple. Large image files over 10 MB, exotic formats like SVG or GIF—all of this makes direct use of the OpenAI API impossible. So I built a workaround with Python that automatically processes the data and imports the AI-generated texts directly into Shopify.

In the following how-to, I'll show you step by step how you can use this solution for your shop.

Why ALT Texts Are Important

ALT texts are much more than just a mandatory task in e-commerce. They ensure that screen readers can describe images, improve your visibility in Google Image Search, and serve as a fallback if an image doesn't load. With precise ALT texts, you support both accessibility and SEO.

My Experience with Apps—and Why They Weren't Enough

Before I developed my own solution, I tried countless apps from the Shopify App Store. But it quickly became clear: none of them could do the job the way a professional shop needs.

Many apps had strict limits and could only process a small number of images at a time

With larger data volumes, they became unstable or crashed

The automatically generated ALT texts were often too generic—examples like "product image" or "photo of jewelry" help neither SEO nor your users

Non-transparent pricing models: credits expired without it being clear when limits would reset. Support was usually not really helpful

The result: a lot of time invested, but no solution that met the high standards for SEO, accessibility, and scalability.

The Solution: Custom Automation with AI

At this point, I decided to develop my own solution. With a combination of the Shopify API and AI-powered text generation, you can create precise, consistent, and SEO-friendly ALT texts fully automatically.

The principle is simple:

Retrieve all product images from Shopify

Feed the AI with relevant product information (product name, material, color, collection, etc.)

Have a concise ALT text generated

Write the ALT text directly back into the image via API

Rules for Good ALT Texts

To ensure the texts are truly meaningful, I defined some clear rules:

Maximum 125 characters

Descriptive and specific (material, color, product type)

No filler words like "beautiful image of..."

Integrate keywords—but without spam

No repetitions

Example: ❌ "Jewelry" ✔️ "PURELEI charm pendant water lily made of sterling silver, worn on the ear"

The How-to: Step by Step

Overview

1. Collect Products and Images

Query all product images via the Shopify API and store them in a local database.

2. Call AI

Use a prompt containing all product info to generate concise ALT texts.

3. Save ALT Texts

Store the results in a local database like SQLite-DB so nothing gets lost.

4. Write Updates to Shopify

Use the API to write the generated ALT texts directly into the respective images.

5. Synchronize Database

Details

1. Collect Products and Images

Goal Retrieve all product images from Shopify, structure them cleanly, and store them locally.

How

The script fetches products via iter_products, extracts images, and builds ImageRef objects for them.

Relevant fields: product_id, image_id, src, alt, product_title, vendor, variants.

Unsupported formats are filtered out directly with is_supported_image, such as svg.

Database

db_init creates the jobs table.

When collecting, entries are only written during processing, keeping the DB lean.

You can optionally add a pure inventory table if you want a complete snapshot:

CREATE TABLE IF NOT EXISTS inventory( product_id INTEGER, image_id INTEGER, src TEXT, alt_shop TEXT, title TEXT, vendor TEXT, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(product_id, image_id) );

To fill, directly after collect_images():

for ref in all_refs: conn.execute(""" INSERT OR REPLACE INTO inventory(product_id,image_id,src,alt_shop,title,vendor) VALUES(?,?,?,?,?,?) """,(ref.product_id,ref.image_id,ref.src,ref.alt,ref.product_title,ref.vendor)) conn.commit()

Checks

Count total images, log per batch.

Filter empty src and duplicate image_id.

Check if ONLY_EMPTY is active if you only want to fill missing ALT texts.

2. Call AI

Goal Generate concise, clean ALT texts from product context.

How

build_context fetches material and colors from variants, plus vendor and tags.

prompt_alt builds a concise, rule-based prompt.

call_openai_alt calls the model, respects rate limits, validates the response, and truncates to ALT_MAX.

Quality Rules

Maximum 125 characters, no filler text, clear naming of product type, material, color.

Language can be controlled via ALT_LANG, for example de or en.

JSON enforcement in the response, robust fallback with regex in case the model gets chatty.

Error Cases

429 or network errors, retry is active.

Empty response, then abort with a clean error message.

Unsupported image formats are already filtered before the AI stage.

3. Save ALT Texts

Goal: Traceability, resumption, reporting.

How

Every decision is stored in jobs via db_upsert.

Columns: alt_old, alt_new, status, error, ts.

Typical status values: dry_run, updated, skipped_has_alt, skipped_unsupported, error.

Useful queries

-- All error cases SELECT * FROM jobs WHERE status='error' ORDER BY ts DESC;

-- Check suggestions from the dry run SELECT product_id,image_id,src,alt_old,alt_new FROM jobs WHERE status='dry_run' LIMIT 100;

-- Still open cases SELECT COUNT(*) FROM jobs WHERE status NOT IN ('updated','skipped_has_alt','skipped_unsupported');

4. Write Updates to Shopify

Goal: Reliably bring the generated ALT texts back into the system.

How

update_image_alt sends a PUT to /admin/api/{API_VERSION}/products/{product_id}/images/{image_id}.json with payload {"image":{"id":image_id,"alt":alt_new}}.

Successful updates are logged with status='updated'.

Safe procedure

First run with DRY_RUN=true, check entries by spot check.

Then DRY_RUN=false, small batches, for example BATCH_SIZE=50.

Observe rate limits, better to run constantly than in parallel, for stability.

Typical errors and causes

404, image or product deleted.

403, missing permissions, check admin token.

422, invalid payload, usually wrong ID or formatting.

5. Synchronize Database

Goal: Keep DB and Shopify in sync, detect manual changes, resume without duplicate work.

Sync recipe

Pull current state

Before each run, execute collect_images again to read the current ALT texts from Shopify.

Optionally update the inventory table, see above.

Determine delta

Join jobs and the fresh inventory state, detect manual changes.

Example: An employee has adjusted an ALT text in the backend, this should not be overwritten.

-- Find manual changes since the last run WITH last AS ( SELECT product_id,image_id,MAX(ts) AS last_ts FROM jobs GROUP BY product_id,image_id ) SELECT i.product_id,i.image_id,i.alt_shop AS alt_now,j.alt_new AS alt_suggest FROM inventory i LEFT JOIN jobs j ON j.product_id=i.product_id AND j.image_id=i.image_id LEFT JOIN last l ON l.product_id=i.product_id AND l.image_id=i.image_id AND j.ts=l.last_ts WHERE j.status='updated' AND i.alt_shop IS NOT j.alt_new;

Conflict strategy

If alt_now is not equal to alt_suggest, then

either respect the manual change and mark status='diverged_manual',

or only write back if alt_now is empty.

Rules as configuration, e.g. RESPECT_MANUAL_CHANGES=true.

Build reprocess queue

Reschedule open or failed jobs.

SELECT product_id,image_id FROM jobs WHERE status IN ('error') -- or other statuses ORDER BY ts ASC;

Ensure idempotency

The script skips already completed entries, see done set in main.

On resumption, the pipeline remains stable, no duplicate updates.

Practical views

CREATE VIEW v_todo AS SELECT i.product_id,i.image_id,i.src,i.alt_shop FROM inventory i LEFT JOIN jobs j ON j.product_id=i.product_id AND j.image_id=i.image_id WHERE j.product_id IS NULL OR j.status IN ('error','dry_run');

CREATE VIEW v_changed AS SELECT i.product_id,i.image_id,i.alt_shop AS alt_now,j.alt_new FROM inventory i JOIN jobs j ON j.product_id=i.product_id AND j.image_id=i.image_id WHERE j.status='updated' AND i.alt_shop != j.alt_new;

Tips for Stability and Quality

Safety for character length: clamp_len keeps to 125 characters, only adjust ALT_MAX if your accessibility guidelines allow it.

Language per market: Use different .env files per shop locale, for example ALT_LANG=en for the UK shop.

Cost control: Log tokens per request, optionally in a second table costs, to see AI costs per batch.

CREATE TABLE IF NOT EXISTS costs( ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP, product_id INTEGER, image_id INTEGER, prompt_tokens INTEGER, completion_tokens INTEGER );

Optionally analyze image content: If you want real image descriptions instead of metadata, download the image locally, give the AI a small caption suggestion and combine with product facts. This increases quality, but is more expensive. Metadata-only is usually sufficient and low risk.

Monitoring: Generate a short report, for example number processed, number of errors, top error reasons, average token costs.

Mini Checklist for Go-Live

.env correct, token tested

Run once with DRY_RUN=true, check results

ONLY_EMPTY=true, so as not to overwrite existing good ALT texts

Small batches, watch logs

After the run, spot check in the frontend, then monitor the Images tab in Google Search Console

Comparison: Simple Setup vs. Advanced Setup for ALT Texts in Shopify

Criterion Simple Setup Advanced Setup
Target group Small shops with < 200 images Large shops with several thousand images
Approach Enter ALT texts manually in the Shopify backend or adjust via CSV import Fully automatic generation & entry via AI + Shopify API
Technology No code necessary Python script, API access, database for jobs & logging
AI usage Generate texts individually in ChatGPT and copy AI generates thousands of ALT texts in batches with resumption
Time required High – each image is maintained manually Very low – even 10,000 images can be processed in a few hours
Flexibility Hardly any: changes must be made manually High: rules, prompts, languages, character length freely definable
Risks Error rate due to copy-paste, slightly inconsistent texts Technical setup requires some know-how, but is consistent and scalable
Costs No app costs, only time investment API costs (OpenAI) + one-time setup effort

The Python Script

Example Python script; you will find the complete explanation soon in my members area.

import os, time, json, re, sys, csv, sqlite3, math
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass
from urllib.parse import urlparse

import requests
from dotenv import load_dotenv

# ========= ENV =========
load_dotenv()
SHOPIFY_SHOP   = os.getenv("SHOPIFY_SHOP")                  # shop.myshopify.com
SHOP_TOKEN     = os.getenv("SHOPIFY_TOKEN")                 # Admin API Access Token
API_VERSION    = os.getenv("API_VERSION", "2025-07")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_MODEL   = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
LANG           = os.getenv("ALT_LANG", "de")
ALT_MAX        = int(os.getenv("ALT_MAX", "125"))
BATCH_SIZE     = int(os.getenv("BATCH_SIZE", "50"))
DRY_RUN        = os.getenv("DRY_RUN", "true").lower() == "true"
ONLY_EMPTY     = os.getenv("ONLY_EMPTY", "true").lower() == "true"   # only update images without ALT
INCLUDE_VENDOR = os.getenv("INCLUDE_VENDOR", "true").lower() == "true"
RESUME_DB      = os.getenv("DB_PATH", "alt_text_jobs.db")

# Backoff for API calls
OAI_MIN_INTERVAL = float(os.getenv("OAI_MIN_INTERVAL", "1.2"))  # seconds between OpenAI calls
SHOP_MAX_RETRY   = int(os.getenv("SHOP_MAX_RETRY", "3"))
OAI_MAX_RETRY    = int(os.getenv("OAI_MAX_RETRY", "3"))

# ========= OpenAI minimal Client =========
OAI_URL = "https://api.openai.com/v1/chat/completions"
OAI_HEADERS = {
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json",
}

# ========= Shopify REST =========
SHOP_BASE = f"https://{SHOPIFY_SHOP}/admin/api/{API_VERSION}"
SHOP_HEADERS = {
"X-Shopify-Access-Token": SHOP_TOKEN,
"Content-Type": "application/json",
}

@dataclass
class ImageRef:
product_id: int
image_id: int
src: str
alt: str
product_title: str
vendor: Optional[str]
tags: Optional[str]
variants: List[Dict[str, Any]]

# ========= Persistence =========
def db_init(path: str):
conn = sqlite3.connect(path)
conn.execute("""
CREATE TABLE IF NOT EXISTS jobs(
product_id INTEGER,
image_id   INTEGER,
src        TEXT,
alt_old    TEXT,
alt_new    TEXT,
status     TEXT,
error      TEXT,
ts         TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(product_id, image_id)
);
""")
return conn

def db_upsert(conn, ref: ImageRef, alt_new: Optional[str], status: str, error: Optional[str]):
conn.execute("""
INSERT INTO jobs(product_id,image_id,src,alt_old,alt_new,status,error)
VALUES(?,?,?,?,?,?,?)
ON CONFLICT(product_id,image_id) DO UPDATE SET
alt_old=excluded.alt_old,
alt_new=excluded.alt_new,
status=excluded.status,
error=excluded.error,
ts=CURRENT_TIMESTAMP
""", (ref.product_id, ref.image_id, ref.src, ref.alt, alt_new or "", status, error or ""))
conn.commit()

# ========= Utils =========
def clean_text(s: str) -> str:
s = re.sub(r"\s+", " ", s or "").strip()
return s

def clamp_len(s: str, n: int) -> str:
s = s.strip()
return s if len(s) <= n else s[:n].rstrip()

def is_supported_image(src: str) -> bool:
# optional filter if you want to skip incompatible formats
ext = urlparse(src).path.lower().rsplit(".", 1)[-1] if "." in urlparse(src).path else ""
return ext in {"png", "jpg", "jpeg", "webp", "gif"}

def log(*a):
print(*a, file=sys.stdout, flush=True)

# ========= Shopify: Fetch products and images =========
def shopify_get(url: str, params: Dict[str, Any] = None, retry=SHOP_MAX_RETRY) -> Dict[str, Any]:
for i in range(retry):
r = requests.get(url, headers=SHOP_HEADERS, params=params, timeout=60)
if 200 <= r.status_code < 300:
return r.json()
time.sleep(1.5 * (i + 1))
raise RuntimeError(f"Shopify GET failed {r.status_code} {r.text[:300]}")

def shopify_put(url: str, payload: Dict[str, Any], retry=SHOP_MAX_RETRY) -> Dict[str, Any]:
for i in range(retry):
r = requests.put(url, headers=SHOP_HEADERS, json=payload, timeout=60)
if 200 <= r.status_code < 300:
return r.json()
time.sleep(1.5 * (i + 1))
raise RuntimeError(f"Shopify PUT failed {r.status_code} {r.text[:300]}")

def iter_products(limit=250):
page = 1
while True:
url = f"{SHOP_BASE}/products.json"
data = shopify_get(url, {"limit": limit, "page": page})
products = data.get("products", [])
if not products:
break
for p in products:
yield p
page += 1

def collect_images() -> List[ImageRef]:
refs: List[ImageRef] = []
for p in iter_products():
pid = p["id"]
title = p.get("title") or ""
vendor = p.get("vendor")
tags = p.get("tags")
variants = p.get("variants", [])
for img in p.get("images", []):
ref = ImageRef(
product_id=pid,
image_id=img["id"],
src=img.get("src") or "",
alt=img.get("alt") or "",
product_title=title,
vendor=vendor,
tags=tags,
variants=variants
)
refs.append(ref)
return refs

# ========= Prompting =========
def build_context(ref: ImageRef) -> Dict[str, Any]:
colors = []
materials = []
for v in ref.variants:
for i in range(1, 4):
o = v.get(f"option{i}")
if not o:
continue
o_norm = o.lower()
if any(k in o_norm for k in [""]):
colors.append(o.strip())
if any(k in o_norm for k in [""]):
materials.append(o.strip())
ctx = {
"product_title": clean_text(ref.product_title),
"vendor": clean_text(ref.vendor or ""),
"colors": sorted(set([c for c in colors if c])),
"materials": sorted(set([m for m in materials if m])),
"tags": clean_text(ref.tags or "")
}
return ctx

def prompt_alt(ref: ImageRef) -> str:
ctx = build_context(ref)
vendor_part = f"{ctx['vendor']} " if INCLUDE_VENDOR and ctx["vendor"] else ""
instr = f"""
You write concise ALT texts in {LANG}.
Rules:
1. Maximum {ALT_MAX} characters
2. Descriptive and specific
3. No filler phrases
4. Mention material and color if appropriate
5. No hashtags
6. Do not repeat the word image

Return only JSON with the field alt.

Conclusion

Instead of relying on half-baked app solutions, you can use your own Python AI automation to create ALT texts in Shopify in a scalable, reliable, and SEO-oriented way. This not only saves time but also improves the accessibility and visibility of your products. If you need support, feel free to schedule an appointment here.

E-commerce SEO Case Study for PURELEI

E-commerce SEO Case Study: PURELEI.com +100% visibility in 8 months

Read More

Case study, SEO, strategies

E-commerce SEO Case Study for PURELEI

E-commerce SEO Case Study: PURELEI.com +100% visibility in 8 months [...]

Case study, SEO

Case Study: How We Generated Over €1.2 Million in Revenue with Targeted Blog Content

Brief overview
A fast-growing brand was able to generate over €1.2 million in revenue within 12 months through targeted content strategies and [...]

Automation

Create Apify Account

Tutorial: Create & Start Apify Account
Step 1: Go to apify.com
Open [...]

Analysis, AI, SEO, SEO Tools

AI Prompt Keyword Mapper

0 (0) How to analyze prompts automatically
Nowadays, prompts – that is, [...]

AI, AI Tools, SEO Tools

ChatGPT German: Use Chat GPT for Free Without Registration

ChatGPT, the advanced language model from OpenAI, is revolutionizing the way [...]

SEO, Shopify

Part 2: Automating Shopify Redirects: Connecting Python Sitemaps, Excel & Matrixify

0 (0) 📖 Part of a series: Why missing redirects cost you traffic [...]

Use my SEO roadmap to get on page 1 of Google!

Sign up for my newsletter and get access to free guides, checklists, and tools.

 
Cookie-Settings