LLM-powered tools that analyze websites for WCAG accessibility issues and generate structured remediation reports. A Computing for Good course project at Georgia Tech (OMSCS) partnered with the Vision Aid Digital Accessibility Testing Team.
There are two ways to use it: a web app (paste a URL or HTML, get findings and a CSV) and a CLI pipeline for batch or scripted runs. Both share the same analysis code.
The pipeline takes a raw HTML file and produces targeted accessibility findings through four steps:
HTML file (e.g. 1.9 MB)
β
β Step 0 β Programmatic Checks (no API cost)
β Rule-based detection of missing alt, empty links, duplicate IDs, etc.
β
β Step 1 β Extract
β Three extractors parse the HTML into structured JSON payloads,
β discarding layout noise (CSS, scripts, divs) and keeping only
β semantically relevant content. ~92% token reduction.
β
βββ semantic_checklist_01.py β headings, links, landmarks, tables, iframes
βββ forms_checklist_02.py β form fields, labels, groups, instructions
βββ nontext_checklist_03.py β images, SVGs, icon fonts, media
Combined: ~39k tokens (from ~487k)
β
β Step 2 β Slice & Call
β Each payload is sliced into targeted pieces. Each slice is paired
β with a focused prompt template and sent to the LLM individually.
β Up to 18 element-specific calls + 3 optional summary calls.
β
β Step 3 β Save
β Raw JSON results are saved per-prompt for downstream processing.
β
β Step 4 β Report
β The report generator reads all saved results, normalizes findings
β from both programmatic and LLM sources, and writes a unified CSV.
βββ index.html # Web UI + team site (all markup/CSS/JS inline)
βββ styles.css
β
βββ entry_points/ # Entry points
β βββ api_server.py # Serves the site and the /api/* audit endpoints
β βββ run_pipeline.py # Runs the full pipeline from the CLI
β βββ generate_report.py # Combines findings into unified CSV report
β
βββ processing_scripts/
β βββ llm/ # Modular prompt system + templates
β β βββ registry.py # Maps each evaluation task to its template + slicer
β β βββ templates.py # Parses .txt prompt files, fills {payload} placeholders
β β βββ slicers.py # Extracts targeted JSON slices from extractor payloads
β β βββ semantic_checklist_01.txt # 7 prompts for semantic structure
β β βββ forms_checklist_02.txt # 6 prompts for form accessibility
β β βββ nontext_checklist_03.txt # 8 prompts for non-text content
β β
β βββ llm_client/ # Standalone Claude API client (Andrew)
β β βββ client.py # API wrapper
β β βββ prompt_loader.py # Prompt loading utilities
β β βββ runner.py # End-to-end audit runner
β β
β βββ llm_preprocessing/ # HTML β structured JSON extractors
β β βββ semantic_checklist_01.py # Headings, links, landmarks, tables, iframes
β β βββ forms_checklist_02.py # Form fields, label associations, groups
β β βββ nontext_checklist_03.py # Images, SVGs, icon fonts, media
β β
β βββ programmatic/ # Rule-based checks (no LLM needed)
β β βββ semantic_checklist_01.py # Semantic structure checks
β β βββ forms_checklist_02.py # Form accessibility checks
β β βββ nontext_checklist_03.py # Non-text content checks
β β
β βββ docs/pipeline.md # Pipeline architecture documentation
β
βββ vision_aid/ingestion/
β βββ file_crawler.py # fetch_page / fetch_pages_nested (used by the server)
β βββ pull_html.py # Standalone HTML download helper
β
βββ Dockerfile # Multi-stage uv build β runtime image
βββ docker-compose.yml # Local run (Coolify deploys the image directly)
βββ DEPLOY.md # Coolify deployment notes
β
βββ .github/workflows/
β βββ ci.yml # On PR to main: deps, imports, pipeline, image smoke test
β βββ publish.yml # On push to main: build β GHCR β trigger Coolify
β
βββ test_files/ # HTML files to analyze
β βββ home.html # visionaid.org homepage (~1.9 MB)
β βββ dat_visionaid_home.html # Smaller trimmed variant (~143 KB)
β
βββ semantic_checklist/ # Source of truth: Deque WCAG checklist PDFs
β βββ 01-semantic-checklist.pdf
β βββ 02-forms-checklist.pdf
β βββ 03-nontext-checklist.pdf
β
βββ pipeline_walkthrough.ipynb # Colab-compatible step-by-step pipeline notebook
β
βββ docs/ # Architecture documentation
β βββ modular-prompts-plan.md # Full architectural plan
β
βββ reports/ # Raw JSON output from standalone llm_client runs
β
βββ test_results/
β βββ chatgpt/ # Legacy ChatGPT testing results
β βββ claude/ # Pipeline-generated CSV reports
β βββ report_YYYY-MM-DD.csv
β
βββ output/ # Generated at runtime (not committed)
βββ manifest.json
βββ programmatic_findings.json
βββ payloads/
βββ prompts/
Each extractor has an extract(file_path) function that parses HTML with BeautifulSoup and returns a structured dict:
| Extractor | Focus | Output tokens (visionaid.org) |
|---|---|---|
semantic_checklist_01.py |
Page title, headings, links, landmarks, tables, iframes | ~17,600 |
forms_checklist_02.py |
Form fields with label source, instructions, required flags | ~2,500 |
nontext_checklist_03.py |
Images (4 categories), SVGs, icon fonts, video/audio | ~19,200 |
These files live in processing_scripts/llm_preprocessing/ and were authored by ahildebrandt3 and Andrew Yin. They should not need modification unless a new checklist (CL04+) is added.
The core of the modular system is in processing_scripts/llm/:
-
registry.pyβ Defines 21PromptSpecdataclass entries, each linking a prompt name to its template file, slicer function, WCAG criteria, and output type. This is the single source of truth for what the pipeline evaluates. -
slicers.pyβ Contains one function per prompt (e.g.,slice_headings(),slice_flagged_links()) that extracts exactly the data that prompt needs from the full extractor payload. This is what achieves the token reduction. -
templates.pyβ Parses the.txtprompt template files (which contain multiple numbered prompts separated by dashed headers) and fills in the{payload}placeholder with the sliced JSON at runtime.
entry_points/run_pipeline.py ties everything together:
- Runs the three extractors to get structured payloads
- Runs programmatic checks on the CL01 payload
- Iterates over the prompt registry, slicing payloads and assembling prompts
- Calls the Anthropic API for each non-empty prompt (or saves dry-run output)
- Writes a manifest with token counts, timing, and cost data
entry_points/generate_report.py reads the pipeline output and produces a flat CSV:
- Loads
manifest.jsonfor run metadata (date, model) - Normalizes
programmatic_findings.json(59 rule-based issues) into report rows - For each
output/prompts/*.json, applies a prompt-specific normalizer that understands the response schema and extracts issues - Assigns sequential IDs and writes to
test_results/claude/report_YYYY-MM-DD.csv
The normalizer registry mirrors the prompt registry β one normalizer function per prompt type that knows how to detect issues in that prompt's response shape.
- Slicer β Add a function in
processing_scripts/llm/slicers.pythat extracts the relevant data from the extractor payload - Template β Add a new numbered prompt section to the appropriate
.txtfile inprocessing_scripts/llm/ - PromptSpec β Add an entry in
processing_scripts/llm/registry.pylinking the slicer, template, and WCAG criteria - Normalizer β Add a normalizer function in
entry_points/generate_report.pyand register it in theNORMALIZERSdict
- Create a new extractor in
processing_scripts/llm_preprocessing/with anextract(file_path)function - Create corresponding prompt templates in
processing_scripts/llm/ - Add slicer functions in
processing_scripts/llm/slicers.py - Register new
PromptSpecentries inprocessing_scripts/llm/registry.py - Add normalizers in
entry_points/generate_report.py - Update
entry_points/run_pipeline.pyto call the new extractor
This project uses uv. It installs the right
Python version itself, so there is no separate Python install or venv step.
# Install uv once (see the uv docs for Windows/other options)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create the environment and install exactly the locked dependencies
uv syncThen either prefix commands with uv run, or activate the environment:
uv run python entry_points/run_pipeline.py --help
# or
source .venv/bin/activate # Linux/macOS
# or: .venv\Scripts\activate # Windowsuv.lock pins every dependency, including transitive ones, so everyone gets
an identical environment. To change a dependency, edit pyproject.toml and run
uv lock, then regenerate the pip fallback below.
Using pip instead (graders, Colab, or no uv available)
requirements.txt is a generated export of uv.lock β do not edit it by
hand; regenerate it with the command in its header. Python 3.11+ required.
python -m venv venv
source venv/bin/activate # Linux/macOS
# or: venv\Scripts\activate # Windows
pip install -r requirements.txt
pip install -e . --no-depsIf python -m venv fails with an ensurepip error, your Python is missing its
venv module (sudo apt install python3-venv on Debian/Ubuntu). uv avoids this
problem entirely.
Create a .env file with your provider API key(s) (only needed for live runs, not dry-run):
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=AIza...
.env is gitignored. Never commit a key.
A run with no resolvable key silently becomes a dry run β programmatic checks only, no LLM findings, no CSV, and still a
200 OKfrom the web app. The only signal issummary.dry_runin the response.
uv run python entry_points/api_server.py # http://localhost:8000Serves the UI and the audit API from one process. Users can paste their own API key into the form instead of configuring one server-side; per-request keys take priority over the environment.
The audit endpoints stream NDJSON β progress events, one JSON object per
line, then a final {"type":"result"} object. Parsing that body with a single
res.json() fails; the front end branches on content type.
To run it in a container:
docker compose up --build # http://localhost:8000
HOST_PORT=8789 docker compose up --build # if 8000 is takenMerges to main build the image, push it to ghcr.io/c4g/va-dat, and trigger a
Coolify deploy to https://va-dat.c4g.dev. See DEPLOY.md β in
particular the proxy settings, since response buffering breaks the progress
stream and short read timeouts cut off long audits.
Note that Coolify runs the image; the hardening in docker-compose.yml
(read_only, tmpfs) applies to local runs only.
.github/workflows/ci.yml runs on every PR to main and needs no API key β
everything it does is free:
uv.lockis in sync withpyproject.toml, andrequirements.txtmatches the lock- entry points import
- a full pipeline dry run, asserting prompts generated, findings found, and zero tokens consumed
index.html's inline JavaScript parses- the Docker image builds, becomes healthy, serves the site, and returns a valid NDJSON audit
Generates all prompts and saves them as JSON files so you can inspect them before spending money:
uv run python entry_points/run_pipeline.py --html test_files/dat_visionaid_home.html --dry-runSends prompts to the LLM and saves responses:
uv run python entry_points/run_pipeline.py --html test_files/home.htmlAfter a live run, combine all findings into a single CSV:
uv run python entry_points/generate_report.py
uv run python entry_points/generate_report.py --output-dir ./output --report-dir ./test_results/claude/| Flag | Default | Description |
|---|---|---|
--html |
(required) | Path to the HTML file to analyze |
--output-dir |
./output |
Directory for results |
--model |
claude-sonnet-5 |
Anthropic model to use |
--dry-run |
off | Generate prompts without calling the API |
--include-summaries |
off | Include the 3 cross-cutting summary prompts |
--show-cost |
off | Print estimated dollar cost of the run based on model pricing |
--env-file |
.env |
Path to environment file |
| Flag | Default | Description |
|---|---|---|
--output-dir |
./output |
Directory containing pipeline output |
--report-dir |
./test_results/claude/ |
Directory to write the CSV report |
output/
βββ manifest.json # Run metadata, token counts, prompt status
βββ programmatic_findings.json # Rule-based checker results (free)
βββ payloads/ # Raw extractor output (for inspection)
β βββ cl01_payload.json
β βββ cl02_payload.json
β βββ cl03_payload.json
βββ prompts/ # One file per prompt
βββ page_title.json # Contains prompt text, payload slice, and API response
βββ heading_structure.json
βββ link_clarity.json
βββ ...
The report CSV has 13 columns matching the Vision Aid team's standard format:
| Column | Description |
|---|---|
ID |
Sequential row number |
element_name |
HTML element (e.g. <img class="...">, <a> "link text") |
browser_combination |
Always N/A (static HTML analysis) |
page_title |
Page title from the analyzed HTML |
issue_title |
Short issue description |
steps_to_reproduce |
Element snippet or inspection steps |
actual_result |
What was found |
expected_result |
What WCAG requires |
recommendation |
Suggested fix |
wcag_sc |
WCAG success criterion (e.g. 1.1.1) |
category |
Issue category (e.g. Programmatic / Non-text Content) |
log_date |
Date of the pipeline run |
reported_by |
Programmatic or the LLM model string |
For visionaid.org homepage (using Claude Sonnet):
| Approach | Input tokens | Cost |
|---|---|---|
| Monolithic (entire HTML) | ~487,000 | ~$1.52 |
| Element-specific pipeline | ~18,000 | ~$0.32 |
The pipeline skips prompts with empty payloads (e.g., no forms on the page = no form prompts), so actual cost varies by page content.
| Contributor | What they own | Key files |
|---|---|---|
| ahildebrandt3 | Extractors, programmatic checkers (CL01βCL03), CL01 prompts | processing_scripts/llm_preprocessing/, processing_scripts/programmatic/ |
| Andrew Yin | CL02 + CL03 extractors, CL02 + CL03 prompts, LLM client, pipeline docs | processing_scripts/llm_preprocessing/, processing_scripts/llm_client/, processing_scripts/llm/*.txt |
| nfulton99 | HTML ingestion, packaging | vision_aid/ingestion/pull_html.py, pyproject.toml |
| ColeANiblett | Pipeline orchestration, prompt system, report generator | processing_scripts/llm/{registry,slicers,templates}.py, entry_points/, docs/ |