Automate Image Alt Text with n8n + Ollama (AI Accessibility Workflow)
Missing or poor alt text is the #1 accessibility violation on the web. WebAIM’s 2025 study found that 54% of all website images lack adequate alternative text. For businesses, that means legal risk under the ADA, EAA (European Accessibility Act), and WCAG 2.2 guidelines — and it means millions of screen reader users can’t interact with your content.
Fixing this manually is painful. A mid-size e-commerce store with 10,000 product images would take weeks of human effort to audit and describe. Cloud vision APIs (Google Vision, Azure, AWS Rekognition) charge $1–4 per 1,000 images and send all your product photos to third-party servers.
With n8n and Ollama, you can generate WCAG-compliant alt text at scale on your own hardware — for free, with full privacy. This tutorial builds a workflow that:
- Scans your website, CMS, or image directory for images missing alt text
- Sends each image to a local vision model (LLaVA) running on Ollama
- Generates context-aware, descriptive alt text following WCAG 2.2 guidelines
- Validates the output for length, quality, and relevance
- Updates your CMS, database, or exports a CSV for bulk import
Why Local AI for Alt Text Generation?
| Feature | Cloud Vision APIs | Self-Hosted (n8n + Ollama) |
|---|---|---|
| Cost per 1,000 images | $1–4 | $0 (your hardware) |
| 10,000 images | $10–40 | $0 |
| 100,000 images | $100–400 | $0 |
| Image data privacy | Sent to cloud provider | Never leaves your server |
| Rate limits | Yes (varies by provider) | None |
| WCAG-specific output | Generic captions | Custom prompts for compliance |
| Customizable for domain | Limited | Full prompt control per image type |
Prerequisites
- n8n — self-hosted instance (Docker or npm)
- Ollama — running locally with a vision model:
llava:13b(best quality) orllava:7b(faster) - Image source — website URL, CMS API (WordPress, Shopify), or local directory
- ~8GB RAM for LLaVA 7B, ~16GB for LLaVA 13B
Pull the vision model:
ollama pull llava:13b
# Or for faster processing with less RAM:
ollama pull llava:7b
Architecture Overview
Image Source (Website crawl / CMS API / Directory scan)
↓
[Find Images] — Identify images with missing or empty alt text
↓
[Download Image] — Fetch image binary for analysis
↓
[Analyze Image] — LLaVA vision model generates description
↓
[Format Alt Text] — Apply WCAG guidelines (length, context, decorative check)
↓
[Validate] — Quality check: not too long, not too generic, relevant
↓
[Output] — Update CMS, write to database, or export CSV
WCAG 2.2 Alt Text Guidelines
Before building the workflow, understand what makes good alt text according to W3C guidelines:
- Be specific and concise — describe the image content in 125 characters or fewer (screen readers may truncate longer text)
- Convey function, not just appearance — “Submit order button” not “green rectangle”
- Skip “image of” or “photo of” — screen readers already announce it as an image
- Mark decorative images — use
alt=""for purely decorative images (borders, spacers, backgrounds) - Include relevant context — a product image alt text should include product name and key features
- Don’t repeat surrounding text — if the caption already says “Red Nike Air Max”, the alt text should add different detail
Step-by-Step Build
Find images that need alt text
Use an HTTP Request node to crawl your site, or connect to your CMS API to find images with missing alt text.
Option A: WordPress REST API
GET https://yoursite.com/wp-json/wp/v2/media?per_page=100&mime_type=image/jpeg
// Filter in n8n Function node:
const needsAlt = items.filter(item =>
!item.json.alt_text || item.json.alt_text.trim() === ''
);
return needsAlt;
Option B: Shopify API (product images)
GET https://yourstore.myshopify.com/admin/api/2024-01/products.json?fields=id,title,images
// Extract images missing alt text:
const images = [];
for (const product of items) {
for (const img of product.json.images) {
if (!img.alt || img.alt.trim() === '') {
images.push({
json: {
product_id: product.json.id,
product_title: product.json.title,
image_id: img.id,
image_url: img.src,
current_alt: img.alt || ''
}
});
}
}
}
return images;
Option C: HTML crawl
// Use HTTP Request to fetch page HTML, then parse with Function node:
const cheerio = require('cheerio'); // n8n has cheerio built in
const $ = cheerio.load(items[0].json.data);
const missingAlt = [];
$('img').each((i, el) => {
const alt = $(el).attr('alt');
const src = $(el).attr('src');
if (!alt || alt.trim() === '') {
missingAlt.push({
json: { src, page_url: items[0].json.url, index: i }
});
}
});
return missingAlt;
Fetch image binary for vision model analysis
Use an HTTP Request node to download each image as binary data:
// HTTP Request node settings:
Method: GET
URL: {{ $json.image_url }}
Response Format: File
Output Field: image_data
For local directories, use the Read Binary File node to load images directly.
Generate descriptions with LLaVA via Ollama
This is the core step. Send the image to Ollama’s vision model with a WCAG-optimized prompt:
// HTTP Request node to Ollama API:
POST http://localhost:11434/api/generate
{
"model": "llava:13b",
"prompt": "You are an accessibility expert writing alt text for screen readers. Describe this image following WCAG 2.2 guidelines:\n\n1. Be specific and concise (under 125 characters)\n2. Describe the content and function, not just appearance\n3. Do NOT start with 'image of', 'photo of', or 'picture of'\n4. If this is a decorative/background image with no informational content, respond with exactly: DECORATIVE\n5. Include relevant details: colors, text visible in image, actions depicted\n\nContext: This image is from a {{ $json.page_context || 'website' }}.\nProduct name (if applicable): {{ $json.product_title || 'N/A' }}\n\nWrite ONLY the alt text, nothing else.",
"images": ["{{ $binary.image_data.base64 }}"],
"stream": false,
"options": {
"temperature": 0.3,
"num_predict": 80
}
}
Customize prompts for different image types
Different contexts need different descriptions. Use a Switch node to route images to specialized prompts:
// Function node: choose prompt based on context
const context = $json.page_context || 'general';
const prompts = {
'product': `Describe this product image for an online store listing.
Include: product type, color, material, key features visible.
Format: "[Product feature] [color/material] [product type]"
Example: "Stainless steel insulated water bottle with flip-top lid, 32oz, matte black finish"
Keep under 125 characters.`,
'blog': `Describe this image for a blog article.
Focus on what the image communicates in the context of the article.
Be descriptive but concise. Under 125 characters.`,
'icon': `This is a UI icon or button image.
Describe its function, not its appearance.
Example: "Search", "Close menu", "Add to cart"
If purely decorative, respond: DECORATIVE`,
'chart': `Describe this chart or graph for someone who cannot see it.
Include: chart type, what is being measured, key trends or values.
Example: "Bar chart showing monthly revenue from Jan to Dec 2025, trending upward from $10K to $45K"`,
'general': `Describe this image concisely for a screen reader user.
Be specific about content. Under 125 characters.
Do not start with "image of" or "photo of".`
};
return [{ json: { ...items[0].json, prompt: prompts[context] || prompts.general } }];
Quality check the generated alt text
// Function node: validate and clean alt text
const altText = $json.generated_alt.trim();
const result = { ...items[0].json };
// Check if decorative
if (altText === 'DECORATIVE' || altText.toLowerCase().includes('decorative')) {
result.final_alt = '';
result.is_decorative = true;
return [{ json: result }];
}
let cleaned = altText;
// Remove "image of", "photo of" prefixes
cleaned = cleaned.replace(/^(an? )?(image|photo|picture|photograph|screenshot|icon) (of|showing|depicting) /i, '');
// Capitalize first letter
cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
// Remove trailing period (screen readers add their own pause)
cleaned = cleaned.replace(/\.$/, '');
// Enforce max length (125 chars for screen reader compatibility)
if (cleaned.length > 125) {
cleaned = cleaned.substring(0, 122) + '...';
}
// Quality flags
result.final_alt = cleaned;
result.is_decorative = false;
result.char_count = cleaned.length;
result.quality_warnings = [];
if (cleaned.length < 10) result.quality_warnings.push('too_short');
if (cleaned.length > 125) result.quality_warnings.push('too_long');
if (/^(a |the |an )/i.test(cleaned)) result.quality_warnings.push('starts_with_article');
if (/image|photo|picture/i.test(cleaned)) result.quality_warnings.push('contains_image_word');
return [{ json: result }];
Write alt text back to your CMS or export
Option A: Update WordPress
// HTTP Request node:
POST https://yoursite.com/wp-json/wp/v2/media/{{ $json.image_id }}
{
"alt_text": "{{ $json.final_alt }}"
}
Option B: Update Shopify product images
// HTTP Request node:
PUT https://yourstore.myshopify.com/admin/api/2024-01/products/{{ $json.product_id }}/images/{{ $json.image_id }}.json
{
"image": {
"id": {{ $json.image_id }},
"alt": "{{ $json.final_alt }}"
}
}
Option C: Export CSV for manual review
// Spreadsheet File node:
// Columns: image_url, current_alt, generated_alt, is_decorative, char_count, quality_warnings
// Output: alt-text-audit-2026-03-24.csv
Batch Processing for Large Sites
Processing 10,000+ images requires careful batching to avoid overwhelming Ollama:
// Function node: batch controller
const BATCH_SIZE = 20;
const DELAY_MS = 1000; // 1 second between batches
const allImages = items;
const batches = [];
for (let i = 0; i < allImages.length; i += BATCH_SIZE) {
batches.push(allImages.slice(i, i + BATCH_SIZE));
}
// Process batches with the Loop Over Items node
// Set batch size to 20, add a Wait node (1s) between batches
// LLaVA processes ~2-5 images/second on CPU, 10-20/second on GPU
- CPU (8-core): ~3 seconds per image → 1,200 images/hour
- GPU (RTX 3060): ~0.5 seconds per image → 7,200 images/hour
- GPU (RTX 4090): ~0.2 seconds per image → 18,000 images/hour
Scheduled Audits: Keep Alt Text Current
New images are added constantly. Set up a Cron trigger to run the workflow weekly:
// Cron node: Run every Sunday at 2 AM
// Only process images added/modified since last run
// Function node: filter by date
const lastRun = $workflow.staticData.lastRun || '2000-01-01';
const newImages = items.filter(img =>
new Date(img.json.date_created) > new Date(lastRun)
);
// Save timestamp for next run
$workflow.staticData.lastRun = new Date().toISOString();
return newImages;
Compliance Reporting
Generate an accessibility audit report after each run:
// Function node: generate report
const total = items.length;
const decorative = items.filter(i => i.json.is_decorative).length;
const generated = items.filter(i => !i.json.is_decorative && i.json.final_alt).length;
const warnings = items.filter(i => i.json.quality_warnings?.length > 0).length;
const report = {
date: new Date().toISOString().split('T')[0],
total_images_scanned: total,
decorative_images: decorative,
alt_text_generated: generated,
needs_review: warnings,
compliance_rate: ((generated + decorative) / total * 100).toFixed(1) + '%'
};
return [{ json: report }];
Send the report via email, Slack, or save to a dashboard. Track your compliance rate over time to demonstrate WCAG progress to stakeholders.
Real-World Use Cases
- E-commerce stores — Generate alt text for product catalogs. Shopify, WooCommerce, Magento, BigCommerce. Improves SEO (Google indexes alt text) and accessibility compliance.
- News and media sites — Describe editorial photos, infographics, and embedded charts for screen reader users.
- Documentation platforms — Generate descriptions for technical screenshots, architecture diagrams, and UI mockups.
- Education — Make course materials accessible by describing slides, diagrams, and educational images.
- Healthcare portals — Describe medical images locally without sending patient data to cloud APIs (HIPAA compliance).
- Government websites — Meet Section 508 and WCAG 2.2 AA requirements for all published images.
SEO Benefits of Alt Text
Alt text isn’t just for accessibility — it directly impacts search rankings:
- Google Image Search — alt text is the primary signal for image search ranking. Proper alt text gets your images into Google Image results, driving additional traffic.
- Page relevance — alt text helps search engines understand page content, improving overall page ranking for target keywords.
- Featured snippets — pages with proper image descriptions are more likely to appear in featured snippets and rich results.
Want the Complete Workflow?
The Self-Hosted AI Workflow Pack includes 11 production-ready n8n templates — including image analysis, content generation, document processing, and more. All powered by Ollama. Zero API costs.
Get 11 AI Workflows — $39One-time payment. No subscription. 30-day guarantee.
Troubleshooting
LLaVA not generating good descriptions
- Use
llava:13binstead ofllava:7bfor better quality - Ensure temperature is 0.3 or lower for factual descriptions
- Add domain context to the prompt (“this is a product image for an electronics store”)
Ollama connection issues from n8n
- If n8n is in Docker, use
http://host.docker.internal:11434instead oflocalhost - Set
OLLAMA_HOST=0.0.0.0in Ollama’s environment to allow network access - Check firewall rules:
sudo ufw allow 11434/tcp
Processing too slow
- Resize images before sending to Ollama (768px max dimension is sufficient for alt text)
- Use
llava:7bfor 2–3x faster processing at slightly lower quality - Process in parallel batches if you have a GPU with enough VRAM