Write Alt Text for Photos with Local AI
Generate photo alt text locally with Ollama, Python, and an HTML gallery.
Introduction
30 Second Summary
A folder full of personal photos becomes difficult to share when every image needs a useful description. Writing those descriptions one by one takes time.
In this project, you will run LFM3-VL locally through Ollama to write one-sentence alt text for every photo in a folder. A Python script will turn those descriptions into a CSV file plus an HTML gallery without uploading your images.
What You'll Build
Your finished gallery will show each photo beside its generated description with non-empty alt text attached to every image.
By the end of this project, you'll have:
- Private local descriptions for every JPG or PNG image in a folder without sending files to an online service.
- A reusable Python script that turns a folder of photos into CSV records containing filenames plus alt text.
- An accessible HTML gallery that pairs every photo with its generated caption while exposing non-empty alt text to accessibility tools.
- Secret Mission: Add automatic image resizing so large photos generate descriptions faster on a laptop.
Are there any prerequisites?
Your laptop needs macOS, Windows, or Linux with Python 3 installed. It also needs about 2 GB of free disk space.
A folder containing JPG or PNG images gives the script something to describe. The project costs nothing because it requires no account, API key, or payment card.
Before We Start
A local vision model can describe photos without sending them away from your laptop. It still needs a dependable Python 3 environment to turn those descriptions into files later.
Your test images also need a predictable home. This step prepares your laptop plus a local photo folder before the model arrives.
In this step, get ready to:
- Confirm that Python 3 runs in your terminal.
- Confirm that your browser can keep the project instructions open.
- Prepare a local photos/ folder containing one or more images you own.
Confirm your laptop tools
This project page proves that your browser is working. A version check confirms that your terminal can find Python before you write the script.
macOS
- Use macOS application search to open a terminal window.
- Confirm your Python version by running this command:
python3 --version
What does this check?
- The python3 command selects the Python 3 interpreter.
- The --version option prints the installed version before the interpreter exits.
Windows
- Use Windows application search to open a terminal window.
- Confirm your Python version by running this command:
python --version
What does this check?
- The python command launches the default Python interpreter.
- The --version option prints the installed version before the interpreter exits.
Linux
- Use your application launcher to open a terminal window.
- Confirm your Python version by running this command:
python3 --version
What does this check?
- The python3 command selects the Python 3 interpreter.
- The --version option prints the installed version before the interpreter exits.
The result determines whether your laptop is ready or needs the official installer.
✔️ The check shows Python 3
That is your first prerequisite confirmed. Python is ready to run the photo-processing script later.
ⓧ Python is missing
A missing command means Python needs to be installed before you continue.
- Visit the official Python downloads page in your browser.
- Choose the download for your operating system.
- Download a supported Python 3 installer.
Installer Permission
An approval prompt can feel more serious than this setup is. It only authorizes the local Python installation.
Your password stays with your operating system.
- Run the downloaded installer with its standard options.
- Approve the operating system permission prompt if it appears.
- Return to the terminal window from earlier.
- Run the version check from your operating system tab again.
You should now see a Python version beginning with 3.
Installation Still Failing?
- Close the terminal window so the finished installation can update your command path.
- Use your operating system's application search to open a fresh terminal window.
- Use the installer guidance for your operating system on the official downloads page if the command still cannot find Python.
Help me finish my Python installation.
Prepare your photo folder
The later script scans one folder for images in the JPG or PNG format. Keeping your test set in photos/ gives the script one predictable place to look.
macOS
- Click the Finder icon in the Dock.
- Select Desktop in the sidebar.
- Choose File in the menu bar.
- Select New Folder.
- Enter photos as the folder name.
- Press Return to create the folder.
- Choose at least one photo you own in JPG or PNG format.
- Drag the selected photo into photos/.
Windows
- Press Windows+E to open File Explorer.
- Select Desktop in the left navigation pane.
- Select New in the toolbar.
- Select Folder.
- Enter photos as the folder name.
- Press Enter to create the folder.
- Choose at least one photo you own in JPG or PNG format.
- Drag the selected photo into photos/.
Linux
- Open your file manager from the application launcher.
- Select the Desktop directory.
- Use the file manager's new-folder control.
- Enter photos as the folder name.
- Confirm the folder name to create it.
- Choose at least one photo you own in JPG or PNG format.
- Drag the selected photo into photos/.
Your Photos Stay Local
The original image files remain on your laptop. This preparation does not upload them to a website or cloud service.
Using photos you own keeps every test image within your rights to use.
Before you inspect the folder, how many image files do you expect to see?
- Return to photos/ in your file manager.
- Count the image files shown inside the folder.
- Confirm that each test image uses a .jpg, .jpeg, or .png extension.
You should see at least one image file inside photos/.
Folder Still Empty?
- Check that you dragged the photo into photos/ instead of placing it beside the folder.
- Confirm that the source photo still opens in your file manager.
Help me put a test image in my photos folder.
Your laptop now has every prerequisite in place. Your local photo set is ready for its first description.
Next up, you will install Ollama. Then you will meet the local model in your terminal.
Install Ollama and Run the Vision Model
Your photos stay private only if the model runs on your laptop. This step prepares that local path before any image is described.
Ollama manages local model files on your computer. You will use it to download LFM3-VL and start a terminal chat.
In this step, get ready to:
- Install Ollama on your laptop.
- Pull the 1.7 GB LFM3-VL model into local storage.
- Send a test message through the interactive terminal chat.
Install Ollama
A terminal check reveals whether Ollama is already available. Its result points you to the correct setup path for your laptop.
- Check whether Ollama is available by running this command:
ollama -v
What does this command do?
This asks Ollama to print its installed version. A version number confirms that your terminal can reach the local runtime.
✔️ I see an Ollama version number
Good start: Ollama is available from this terminal. Continue to the confirmation check below.
ⓧ Command not found
Ollama still needs to be installed. Choose your operating system below.
macOS
- Install Ollama by running this command:
curl -fsSL https://ollama.com/install.sh | sh
What does this command do?
- The command downloads Ollama's official installation script.
- Your shell runs the script to install Ollama.
Windows
PowerShell runs the official Windows installation script. The installer places Ollama in your user account.
- Install Ollama from PowerShell by running this command:
irm https://ollama.com/install.ps1 | iex
What does this command do?
- The command downloads Ollama's official PowerShell installation script.
- PowerShell runs the script to install Ollama.
Linux
- Install Ollama by running this command:
curl -fsSL https://ollama.com/install.sh | sh
What does this command do?
- The command downloads Ollama's official installation script.
- Your shell runs the script to install Ollama.
- Confirm that the installation completed by running this command:
ollama -v
What confirms the installation?
Ollama prints its installed version when the command is available. This confirms that your terminal can start the local runtime.
Installation is complete: this terminal can now call Ollama.
Ollama still unavailable?
- Restart your terminal application so it reloads the installed command.
- Run the version check again in the new terminal.
- Confirm that you selected the installation command for your operating system.
Get help diagnosing my Ollama installation. You can also ask the Ollama Discord community.
Pull and test LFM3-VL
LFM3-VL is a vision model that accepts text or images. A text-only chat confirms that the model can run before you give it a photo.
Where does the model run?
Ollama stores the model on your laptop. The model runs through Ollama's local service.
Your existing photos/ folder remains on your laptop. This step does not send any photo to an external service.
The first run downloads 1.7 GB of model files. The download can take a while on a slower connection.
- Pull LFM3-VL into local storage by running this command:
ollama run lfm3-vl
What does this command do?
- Ollama checks its local model storage for LFM3-VL.
- The first run downloads the model when no local copy exists.
- The downloaded model stays available for later runs.
- The command starts an interactive terminal chat after the model loads.
That was the largest setup task: LFM3-VL is now stored locally. The interactive prompt confirms that the model is ready for a message.
The first response can take longer while the model loads into memory. A CPU-only laptop may need extra time before text begins to appear.
Before you send the message, what kind of answer do you expect from a vision model that has received only text?
- Type Describe the kinds of images you can understand in one short sentence at the interactive prompt.
- Press Enter to send the message.
You'll see a text reply from LFM3-VL. The interactive prompt returns after the response.
Model not downloading or responding?
- Confirm that your internet connection is active while Ollama downloads the model.
- Check that the download finished before typing your test message.
- Keep the Ollama local service running while the chat is active.
Help me diagnose why LFM3-VL is not downloading or responding. You can also ask the Ollama Discord community.
Your vision model is stored locally and responding through Ollama. Next, you'll give it one of your own photos and compare its description with what you see.
Describe a Photo in the Terminal
Your earlier chat proved Ollama can answer with the stored LFM3-VL model. A photo test checks whether the model can connect its words to visible details.
This step gives the model one local image before you automate a whole folder. You will judge the answer against the photo itself.
In this step, get ready to:
- Choose one local photo as your test image.
- Ask LFM3-VL for a one-sentence description in the terminal.
- Compare the generated description with the visible contents of the photo.
Build a human baseline
A test photo works best when you already know what it contains. Looking closely first gives you a human baseline for comparison.
- Return to the photos/ folder in your computer's file browser.
- Choose one .jpg or .png file with a clear main subject.
- View the chosen photo at full size.
- Notice the main subject.
- Notice one background detail.
You now have two concrete details that the model can confirm or miss.
The terminal identifies your photo by its full file path. Dragging the file into the terminal reveals that path without changing the image.
- Return to the terminal from earlier.
- Drag your chosen photo into the terminal's empty input line.
- Copy the full file path shown in the terminal.
- Save the copied path here: path to one photo in your photos folder.
- Clear the current terminal input with Ctrl+C on macOS or Windows.
Generate the local description
The command gives the local Ollama service your image path. It also asks for one sentence that you can judge quickly.
Before you send the photo, what detail do you expect the model to mention first? Your prediction becomes the baseline for the result.
Image analysis may take longer than your earlier text chat. The model is processing the photo locally during that wait.
- Pass the image path to the stored model by running this command:
ollama run lfm3-vl [[PHOTO_PATH="path to one photo in your photos folder"]] "Describe this image in one short sentence."
What does this command do?
- The ollama run command sends one prompt to a locally stored model.
- The lfm3-vl value selects the vision model you pulled earlier.
- The saved file path identifies the photo that the model reads.
- The final instruction keeps the response focused on one short description.
The terminal should show a response containing a short sentence about the photo's visible subject or scene.
That is your first local image description working. The original photo stayed on your laptop while LFM3-VL processed it.
No description from the photo?
Check that the saved path points to a .jpg or .png file inside photos/. A moved file leaves the command with an invalid path.
If the response ignores the image, capture its path again by dragging the photo into the terminal. Save the new path before rerunning the command.
Help me diagnose why LFM3-VL is not describing my local photo.
Compare the description with the photo
A useful description names the main subject without inventing details. Your comparison catches confident mistakes before the sentence becomes alt text.
Before you compare them, do you expect every claimed detail to have visible evidence? The photo gives you the answer.
- Return to the same photo.
- Display the photo at full size.
- Read the generated sentence again.
- Match its main subject to the visible subject in the photo.
- Check each descriptive detail against the image.
- Identify any claim with no visible evidence.
- Decide whether the sentence communicates the scene without relying on the image.
You should be able to connect the sentence's main subject to something visible in the photo. Every claimed detail should have evidence in the image.
You have now tested the model with a real photo. You also completed the human quality check that automated alt text still needs.
Your local vision model can now turn an image into a description. Next, you will send the same kind of request through Ollama's local API and inspect the structured response.
Test the Local Ollama API
The terminal description proved that lfm3-vl can inspect a photo stored on your laptop. You now need a repeatable way to send the same task without an interactive chat.
The Ollama local HTTP API provides that connection at http://localhost:11434. It returns JSON that your Python script can read in the next step.
In this step, get ready to:
- Choose a local photo for the API test.
- Send the photo to the Ollama HTTP API with curl.
- Find the generated description in the returned JSON.
Choose a photo for the request
The API request can use the same photo you tested in the terminal. Its path tells the encoding command which local file to read.
- Enter the path to the photo you want to test here: photos/your-photo.jpg.
Why Does curl Need Base64?
The REST API expects the photo as Base64 text inside the JSON request. This encoding gives the image a text representation that the API can receive.
Encoding reads the selected file without changing the original photo in photos/. The request stays on your laptop through the local Ollama service.
Test a local API request
The request combines the encoded image with a prompt for one short sentence. Disabling streaming returns one complete JSON object that is easier to inspect.
macOS
Large photos can take a while to process on a CPU. A quiet terminal during that wait is normal.
Before you run this, do you expect the API description to mention the same subject as your earlier terminal test?
- Encode your selected photo and send it to the local API by running this command:
IMG=$(base64 < [[IMAGE_PATH="photos/your-photo.jpg"]] | tr -d '\n')
curl -X POST http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "lfm3-vl",
"messages": [{
"role": "user",
"content": "Describe this image in one short sentence.",
"images": ["'"$IMG"'"]
}],
"stream": false
}'
What Does This Request Do?
- The IMG variable holds a Base64 copy of the selected photo for this terminal session.
- The images array attaches that encoded photo to the user message.
- The stream value asks Ollama to return one complete JSON object.
- The http://localhost:11434/api/chat endpoint routes the request to the Ollama service on your laptop.
You should see one JSON object printed in the terminal. It contains a message object with the model's answer.
No JSON Response?
- Return to the Ollama process from earlier if its local service has stopped.
- Check that your selected path points to an existing JPG or PNG file inside photos/.
- Check that every quote in the JSON request matches the command shown above.
If the response is still missing, help me debug my local Ollama API request.
Windows
Large photos can take a while to process on a CPU. A quiet PowerShell window during that wait is normal.
Before you run this, do you expect the API description to mention the same subject as your earlier terminal test?
- Encode your selected photo and send it to the local API by running this command in PowerShell:
$IMG = [System.Convert]::ToBase64String(
[System.IO.File]::ReadAllBytes("[[IMAGE_PATH="photos/your-photo.jpg"]]")
)
$BODY = @{
model = "lfm3-vl"
messages = @(
@{
role = "user"
content = "Describe this image in one short sentence."
images = @($IMG)
}
)
stream = $false
} | ConvertTo-Json -Depth 4 -Compress
$REQUEST_FILE = Join-Path $env:TEMP "ollama-request.json"
[System.IO.File]::WriteAllText($REQUEST_FILE, $BODY)
curl.exe -X POST http://localhost:11434/api/chat `
-H "Content-Type: application/json" `
-d "@$REQUEST_FILE"
Remove-Item $REQUEST_FILE
What Does This Request Do?
- The IMG variable holds a Base64 copy of the selected photo for this PowerShell session.
- The BODY variable packages the user message with its image data.
- The temporary JSON file gives curl.exe a complete request body to send.
- The final line deletes the temporary request file after the API responds.
You should see one JSON object printed in PowerShell. It contains a message object with the model's answer.
No JSON Response?
- Return to the Ollama process from earlier if its local service has stopped.
- Check that your selected path points to an existing JPG or PNG file inside photos/.
- Use curl.exe so PowerShell runs the curl command-line tool.
If the response is still missing, help me debug my local Ollama API request.
Linux
Large photos can take a while to process on a CPU. A quiet terminal during that wait is normal.
Before you run this, do you expect the API description to mention the same subject as your earlier terminal test?
- Encode your selected photo and send it to the local API by running this command:
IMG=$(base64 < [[IMAGE_PATH="photos/your-photo.jpg"]] | tr -d '\n')
curl -X POST http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "lfm3-vl",
"messages": [{
"role": "user",
"content": "Describe this image in one short sentence.",
"images": ["'"$IMG"'"]
}],
"stream": false
}'
What Does This Request Do?
- The IMG variable holds a Base64 copy of the selected photo for this terminal session.
- The images array attaches that encoded photo to the user message.
- The stream value asks Ollama to return one complete JSON object.
- The http://localhost:11434/api/chat endpoint routes the request to the Ollama service on your laptop.
You should see one JSON object printed in the terminal. It contains a message object with the model's answer.
No JSON Response?
- Return to the Ollama process from earlier if its local service has stopped.
- Check that your selected path points to an existing JPG or PNG file inside photos/.
- Check that every quote in the JSON request matches the command shown above.
If the response is still missing, help me debug my local Ollama API request.
Read the JSON response
The response includes the model's answer plus details about the completed request. The description sits inside the assistant message.
- Find the message object in the terminal response.
- Read the text stored in its content field.
- Compare the description with the visible contents of your selected photo.
- Find the done field near the end of the response.
- Confirm that its value is true.
You should see a short description inside message.content. The done value should be true.
That is the automation bridge working. Your model now accepts a local photo through an API request without uploading the original file.
Your local API is ready for repeatable image requests. Next up, you will use Python to describe every photo in photos/.
Generate Alt Text for Every Photo
Your local Ollama API already describes a single photo. You have proved that the image stays on your laptop during the request.
Repeating that request by hand would become tedious for a whole folder. A Python script can process each photo through the local model before collecting the descriptions in a CSV file.
In this step, get ready to:
- Connect Python to Ollama with the official ollama package.
- Send every supported photo to the local lfm3-vl model.
- Save each filename with its generated alt text in alt_text.csv.
Connect Python to Ollama
The official Python package turns your earlier local API request into a function call. It uses the Ollama service that is already running on your laptop.
- Install the official package in your Python environment by running this command:
pip install ollama
What does this command do?
This installs the Python client for Ollama. Your script uses that client to send image paths to the local service.
- Wait for the installation process to return control to your terminal.
That package now gives your Python script a direct route to the local model.
Package installation failed?
- Check that pip belongs to the Python environment that runs your scripts.
- Review your terminal output for a network or permissions problem.
Help me resolve the installation problem.
Turn one request into a batch
The batch script filters the contents of photos/ to supported image formats. It asks the model for a short sentence before writing each result to a new row.
- Create generate_alt_text.py with your plain-text editor in the folder that contains photos/.
- Paste the following code into generate_alt_text.py:
import csv
from pathlib import Path
from ollama import chat
# Keep the local model and prompt easy to change.
MODEL = "lfm3-vl"
PROMPT = "Write one short sentence under 125 characters. Return only the alt text."
# Send one local image to the local vision model.
def describe(image):
response = chat(
model=MODEL,
messages=[{
"role": "user",
"content": PROMPT,
"images": [str(image)],
}],
)
return " ".join(response.message.content.split()).strip()[:125]
# Collect supported photos and write one CSV row for each result.
images = sorted(path for path in Path("photos").iterdir() if path.suffix.lower() in {".jpg", ".jpeg", ".png"})
with Path("alt_text.csv").open("w", newline="", encoding="utf-8") as csv_file:
writer = csv.writer(csv_file)
writer.writerow(["filename", "alt_text"])
for image in images:
alt_text = describe(image)
writer.writerow([image.name, alt_text])
print(f"{image.name}: {alt_text}")
What does this script do?
- The MODEL value keeps the local model name in one place.
- The describe() function sends one image path with the alt text prompt.
- The file filter selects images ending in .jpg, .jpeg, or .png.
- The CSV writer stores each image name beside the cleaned model response.
- Save generate_alt_text.py.
- View the containing folder in Finder on macOS or File Explorer on Windows.
You will see generate_alt_text.py beside the existing photos/ folder.
Script saved in the wrong place?
- Move generate_alt_text.py into the folder that directly contains photos/.
- Confirm that your editor saved the file with the .py extension.
Help me check my script location.
✔️ Awesome, I've got everything!
Your saved script matches the batch workflow.
ⓧ I'd like to double check the full code
Compare your saved file with this complete reference.
import csv
from pathlib import Path
from ollama import chat
# Keep the local model and prompt easy to change.
MODEL = "lfm3-vl"
PROMPT = "Write one short sentence under 125 characters. Return only the alt text."
# Send one local image to the local vision model.
def describe(image):
response = chat(
model=MODEL,
messages=[{
"role": "user",
"content": PROMPT,
"images": [str(image)],
}],
)
return " ".join(response.message.content.split()).strip()[:125]
# Collect supported photos and write one CSV row for each result.
images = sorted(path for path in Path("photos").iterdir() if path.suffix.lower() in {".jpg", ".jpeg", ".png"})
with Path("alt_text.csv").open("w", newline="", encoding="utf-8") as csv_file:
writer = csv.writer(csv_file)
writer.writerow(["filename", "alt_text"])
for image in images:
alt_text = describe(image)
writer.writerow([image.name, alt_text])
print(f"{image.name}: {alt_text}")
Generate and inspect the CSV
The script now has everything it needs to process the folder locally. Each model response becomes an alt text value in alt_text.csv.
Generating each description can take a little while on a CPU. A quiet pause while the model examines a photo is normal.
Before you run the batch, how many photo descriptions do you expect the script to print?
- Generate the alt text file by running the script from the folder that contains photos/:
python3 generate_alt_text.py
What happens during the run?
- The script selects each supported image from photos/.
- The local model generates one description for the selected image.
- The script prints the result before saving it to the CSV.
You will see one filename with a generated description for every supported photo. The terminal returns to its prompt after the final image.
- Return to the plain-text editor you used for the script.
- Open alt_text.csv from the same folder.
- Compare each CSV filename with the source files in photos/.
You will see a filename column beside an alt_text column. The file contains one data row for each supported photo that the script processed.
Missing rows or descriptions?
- Confirm that each source image ends in .jpg, .jpeg, or .png.
- Confirm that the source images are directly inside photos/.
- Switch back to the Ollama service from earlier if the script cannot reach the local model.
Help me debug my batch alt text script.
You have turned a folder of local photos into a reusable alt text dataset. Next, you will use those rows to build a gallery that exposes every description in the browser.
Build and Inspect Your Photo Gallery
Your local Ollama model has turned the photos in photos/ into rows in alt_text.csv. A CSV table still hides the connection between each photo and its description.
A local HTML gallery makes those connections visible. You will also inspect each image's alt attribute so the accessibility text is present in the page itself.
In this step, get ready to:
- Generate gallery.html from the rows in alt_text.csv.
- Review every processed photo beside its generated caption.
- Inspect each image's non-empty alt attribute in your browser's developer tools.
Generate the gallery from your CSV
The gallery generator escapes every description before placing it in the page. This prevents punctuation in the generated text from breaking the HTML.
- Create generate_gallery.py beside generate_alt_text.py.
- Build the gallery generator by copying this code into generate_gallery.py:
import csv, html, webbrowser
from pathlib import Path
from urllib.parse import quote
# Store one escaped image card for each CSV row
cards = []
with Path("alt_text.csv").open(newline="", encoding="utf-8") as csv_file:
reader = csv.reader(csv_file)
next(reader)
for filename, alt_text in reader:
image_path = f"photos/{quote(filename)}"
safe_alt_text = html.escape(alt_text, quote=True)
cards.append(
f'<figure><img src="{image_path}" alt="{safe_alt_text}">'
f"<figcaption>{safe_alt_text}</figcaption></figure>"
)
# Build, save, and open the complete local gallery
page = f"""<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Local Photo Gallery</title><style>img{{max-width:min(90vw,720px);height:auto}}figure{{margin:2rem 0}}</style></head>
<body>
<h1>Local Photo Gallery</h1>
{"".join(cards)}
</body>
</html>
"""
output_path = Path("gallery.html")
output_path.write_text(page, encoding="utf-8")
webbrowser.open(output_path.resolve().as_uri())
What does this code do?
- The csv.reader() call reads each filename and generated description from alt_text.csv.
- The next(reader) call skips the CSV header row.
- The quote() call converts each filename into a browser-safe photo path.
- The html.escape() call protects the generated description before it enters the page.
- Each generated sentence becomes a visible caption plus the image's alt value.
- The webbrowser.open() call sends the finished local file to your default browser.
✔️ Awesome, I've got everything!
Great. Double-check that you have saved generate_gallery.py.
ⓧ I'd like to double check the full code
import csv, html, webbrowser
from pathlib import Path
from urllib.parse import quote
# Store one escaped image card for each CSV row
cards = []
with Path("alt_text.csv").open(newline="", encoding="utf-8") as csv_file:
reader = csv.reader(csv_file)
next(reader)
for filename, alt_text in reader:
image_path = f"photos/{quote(filename)}"
safe_alt_text = html.escape(alt_text, quote=True)
cards.append(
f'<figure><img src="{image_path}" alt="{safe_alt_text}">'
f"<figcaption>{safe_alt_text}</figcaption></figure>"
)
# Build, save, and open the complete local gallery
page = f"""<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Local Photo Gallery</title><style>img{{max-width:min(90vw,720px);height:auto}}figure{{margin:2rem 0}}</style></head>
<body>
<h1>Local Photo Gallery</h1>
{"".join(cards)}
</body>
</html>
"""
output_path = Path("gallery.html")
output_path.write_text(page, encoding="utf-8")
webbrowser.open(output_path.resolve().as_uri())
- Save generate_gallery.py.
Before you run the script, how many photo cards do you expect the gallery to contain?
macOS
- Generate gallery.html by running this command:
python3 generate_gallery.py
What does this command do?
Python 3 runs generate_gallery.py. The script writes gallery.html.
Your default browser receives the finished local file.
Your browser should open gallery.html. You should see one photo card for every data row in alt_text.csv.
Windows
- Generate gallery.html by running this command:
python generate_gallery.py
What does this command do?
Python runs generate_gallery.py. The script writes gallery.html.
Your default browser receives the finished local file.
Your browser should open gallery.html. You should see one photo card for every data row in alt_text.csv.
Linux
- Generate gallery.html by running this command:
python3 generate_gallery.py
What does this command do?
Python 3 runs generate_gallery.py. The script writes gallery.html.
Your default browser receives the finished local file.
Your browser should open gallery.html. You should see one photo card for every data row in alt_text.csv.
Gallery did not open?
- Confirm your terminal is using the folder that contains generate_gallery.py plus alt_text.csv.
- Locate gallery.html beside generate_alt_text.py.
- Open gallery.html with your web browser if the browser did not open automatically.
Help me diagnose why my local gallery was not created or opened.
Good progress. Your generated descriptions now have a visual home beside the photos they describe.
Check every photo and caption
A complete gallery has one visual card for every CSV data row. Comparing the counts catches missing photos before you inspect the hidden accessibility text.
- Switch back to alt_text.csv in your editor.
- Count the data rows below the header.
- Return to gallery.html in your browser.
- Count the photo cards in the gallery.
- Compare each visible caption with its matching row in alt_text.csv.
You should see the same number of photo cards as CSV data rows. Every card should show its generated description beneath the matching photo.
Inspect the alt text in your browser
The alt attribute gives assistive technology a text alternative for an image. Browser developer tools reveal the exact value stored on each img element.
Before you inspect the first photo, do you expect its generated description to appear in the caption, the HTML markup, or both?
- Right-click the first photo in gallery.html.
- Select your browser option that inspects the chosen element.
- Locate the highlighted img element in developer tools.
- Find the alt attribute on that element.
- Compare its value with the visible caption beneath the photo.
- Repeat the inspection for every image in the gallery.
You should see a non-empty alt value on every img element. Each value should match the generated caption for that photo.
Alt text missing?
- Confirm that you ran generate_gallery.py after saving its latest version.
- Check that alt_text.csv contains a generated description for the affected filename.
- Confirm that the selected markup is an img element from your gallery.
Help me find why an image in my local gallery has missing alt text.
- Frame the developer tools panel so your photo stays outside any screenshot.
You have closed the loop. Every processed photo now appears in a local gallery with visible caption text plus a verified accessibility description.
Secret mission
Run a Human Alt Text Audit
Generated alt text still benefits from human judgment. Audit every description for accuracy, inspect the HTML attributes, and draft a stronger replacement for the weakest result.
Clean Up Your Resources
Clean Up Your Resources
Everything you created stays on your laptop, so there are no ongoing service charges. Choose whether to keep the model and project files, pause the local service, or remove the stored resources.
Resources you used:
- The lfm3-vl model stored by Ollama.
- The generate_alt_text.py script.
- The generated alt_text.csv file.
- The generated gallery.html file.
- The photos/ folder containing your original images.
Keep everything running
No action is required. Choose this option if you want to generate more alt text or update your gallery.
- Keep the lfm3-vl model in Ollama for future image descriptions.
- Keep generate_alt_text.py for processing more photos.
- Keep alt_text.csv as a record of the generated descriptions.
- Keep gallery.html for viewing the photos with their alt text.
- Keep photos/ with the original images you want to process again.
Pause - I'll come back to this later
Stop the local service to free up memory while preserving everything needed to continue later. Your files and model remain on your laptop at no cost.
- Quit Ollama using the same local control you used to start it.
- Keep the lfm3-vl model installed for your return.
- Store generate_alt_text.py in the same folder as alt_text.csv.
- Store gallery.html with the other project files.
- Leave photos/ beside the script.
The local API stays unavailable while Ollama is closed. It becomes available again the next time Ollama starts.
Delete - I don't want to use this again
Deleting these local resources is permanent. That can feel drastic, so the file cleanup starts by protecting any original photos you still need.
- Remove the local vision model by running this command:
ollama rm lfm3-vl
What does this command do?
This removes the stored model weights and frees about 1.7 GB of disk space. Ollama stays installed.
- Wait for the terminal to return to its prompt.
- Confirm that the command finishes without an error.
That's the largest local resource cleared. The model no longer occupies space on your laptop.
Model Removal Failed?
- Check that Ollama is available locally.
- Run the command above again after Ollama starts.
- Help me understand why Ollama could not remove my local lfm3-vl model.
The remaining resources are normal local files. Removing them clears the script and its generated output.
- Copy any original images you want to preserve out of photos/.
- Press Cmd+Space on macOS or the Windows key on Windows.
- Type Finder on macOS or File Explorer on Windows.
- Press Enter to open the selected file manager.
- Locate the folder containing generate_alt_text.py.
- Select generate_alt_text.py, alt_text.csv, gallery.html, and photos/.
- Use the file manager's delete control to remove the selected resources.
- Empty your system's deleted-items folder to complete the removal.
A final search confirms that the local project files are gone.
- Search your laptop for generate_alt_text.py.
You should see no matching project file. Your local model and project artifacts are now removed.
Nice Work!
Nice Work!
You built a private photo-description workflow powered by Ollama. Your photos stay on your laptop while your local vision model turns them into accessible descriptions.
What you learned:
- Tested the LFM3-VL vision model with your own photo. You also confirmed that Ollama's local API returns the generated description as JSON.
- Built a Python automation script that processes every supported image in photos/. The script records each filename with its generated alt text in alt_text.csv.
- Generated an accessible HTML gallery that displays every processed photo. You verified each description through the browser's accessibility inspector or developer tools.
- Secret Mission: Extended your local photo accessibility workflow through an optional challenge.
Ready to quiz yourself?