Evaluate, curate, and prepare images for upload to Wikimedia Commons. Use this skill whenever the user wants to upload photos to Wikimedia Commons, contribute images to Wikipedia, prepare stock photos for Commons, assess image quality for Commons, create Wikimedia description boxes, generate {{Information}} templates, rename images for Commons conventions, or categorize photos for Wikimedia. Also trigger when the user mentions "Commons upload", "Wikimedia", "Commons-tauglich", "Bilder hochladen", "Commons-Beschreibung", or has a batch of images they want to filter and prepare for contribution. This skill covers the full pipeline from raw image directory to upload-ready files with descriptions — not just one step.
Take a directory of images and upload them to Wikimedia Commons: technically vetted, visually reviewed, deduplicated, renamed, described, categorized, metadata-stripped, and uploaded via Pywikibot.
Ten steps. Steps 1-7 produce the upload-ready set. Steps 8-10 handle the actual upload.
Raw images
→ 1. Resolution check (drop sub-2MP)
→ 2. EXIF extraction (flag technical issues)
→ 3. Format duplicate pruning (.JPG vs .jpeg)
→ 4. Gather location context for image clusters
→ 5. Visual review + tier classification + near-duplicate flagging
→ 6. Resolve near-duplicates (pick best per group)
→ 7. Copy+rename to upload/, strip metadata, generate descriptions
→ 8. Dry-run preview (user confirmation gate)
→ 9. Upload to Commons via Pywikibot
→ 10. Post-upload verification + log
Use these defaults unless the user explicitly overrides them for a given session:
Mike is MichiIf the user mentions a different username, license, or copyright situation, use that instead. Otherwise, proceed directly with these values. Do NOT ask for confirmation each time.
Do NOT ask for location context yet. That comes in step 4 after the visual landscape of the image set is known.
The cheapest filter. Run resolution extraction across all images first (sips, PIL, or exiftool — whichever is fastest on the platform). Drop anything below 2 megapixels.
This is a hard cut, not a flag. Sub-2MP images have no realistic Commons value.
Report: total images, how many dropped, how many survive to step 2.
Run on survivors only. Extract in a single batch operation — not one subprocess per file.
Prefer exiftool -csv for batch extraction. It handles all fields in one pass and
outputs structured data. Fall back to sips+mdls or Python PIL only if exiftool is
unavailable.
| Metric | How to get it |
|---|---|
| Resolution (already have from step 1) | — |
| File size | stat / os.path.getsize |
| ISO | EXIF ISOSpeedRatings / kMDItemISOSpeed |
| Shutter speed | EXIF ExposureTime / kMDItemExposureTimeSeconds |
| Aperture | EXIF FNumber / kMDItemFNumber |
| Focal length | EXIF FocalLength / kMDItemFocalLength |
| Date taken | EXIF DateTimeOriginal / kMDItemContentCreationDate |
| Camera model | EXIF Model (used to set ISO threshold: phone vs camera) |
| Lens ID | EXIF LensModel (used to detect front camera — see step 2b) |
After extracting EXIF, check the LensModel field. On iPhones, the front-facing
camera has a distinct lens identifier (e.g., "iPhone 17 Pro front camera 2.22mm f/1.9"
vs rear lenses). Flag all front-camera images as likely selfies and report
them separately.
These are not auto-dropped, but they are excluded from visual review by default. Present the count and a sample filename to the user. If the user says some front-camera shots are worth reviewing, view those specifically. Otherwise skip them all in step 5.
This saves enormous context on batches where 20-40% of images are personal portraits.
These are flags shown to the user, not automatic rejections.
| Flag | Condition | Why |
|---|---|---|
| High ISO | > 1250 (phone sensor) / > 3200 (dedicated camera) | Noise |
| Slow shutter | > 1/30s handheld | Motion blur risk |
| Small file | < 500 KB for a multi-MP image | Over-compressed |
Determine phone vs camera from the EXIF camera model field. If unavailable, assume phone thresholds (more conservative).
Present a summary:
Save the full report to technical_scan_results.md so it survives context resets.
If the image count is large (100+), suggest the user start a fresh context window before step 5.
Identify files with the same base name but different extensions (e.g., IMG_0302.JPG
and IMG_0302.jpeg). Keep the higher-resolution version, drop the other. Report which
pairs were found and which version was kept.
This is deterministic — no user input needed.
Use EXIF GPS data to identify location clusters, then reverse-geocode each cluster center via the OpenStreetMap Nominatim API to get actual place names automatically.
Group images by date and GPS proximity. Images within ~0.005° (~500m) of each other belong to the same cluster. For each cluster, compute the center lat/lon.
Hit the Nominatim reverse endpoint for each cluster center. This eliminates guesswork and gives real neighborhood/street-level names.
import urllib.request, json, time
clusters = [("A", lat, lon), ...] # from clustering step
for label, lat, lon in clusters:
url = (f"https://nominatim.openstreetmap.org/reverse?"
f"lat={lat}&lon={lon}&format=json&zoom=16&addressdetails=1")
req = urllib.request.Request(url,
headers={"User-Agent": "commons-upload-pipeline/1.0"})
resp = urllib.request.urlopen(req)
data = json.loads(resp.read())
addr = data.get("address", {})
suburb = addr.get("suburb") or addr.get("neighbourhood") or addr.get("quarter") or ""
road = addr.get("road", "")
city = addr.get("city") or addr.get("town") or ""
print(f"Cluster {label}: {suburb}, {road}, {city}")
time.sleep(1.1) # Nominatim requires max 1 req/sec
Rate limit: Nominatim enforces 1 request per second. Always sleep 1.1s between calls. Set a descriptive User-Agent string.
Present a table of clusters with the resolved place names. Ask the user to confirm or correct. The goal: when you reach step 7 (descriptions), every image already has its location pinned down. No retroactive patching.
If any clusters lack GPS data entirely, ask the user for those locations manually. If the Nominatim result is too vague (e.g., just a highway name), note which clusters need refinement and ask during visual review when actual content is visible.
A single pass. View each surviving image, assess it, and immediately classify it. Do not separate "review" and "classification" into distinct steps — they're the same cognitive act.
| Criterion | What to look for |
|---|---|
| Subject matter | Is it identifiable? Would a Wikipedia article use it? |
| Composition | Clean framing, no distracting elements |
| Focus / sharpness | Is the subject in focus? |
| Lighting | Blown highlights, crushed shadows, harsh midday light |
| Obstructions | Cables, poles, fingers, watermarks, logos |
| People | Recognizable faces → model release concern. Flag for user. |
| Redundancy | Near-duplicate of another image in the batch — mark the group |
Present results as a table per tier: filename, subject description, notes on issues. For Tier 2 near-duplicate groups, indicate which images belong to the same group.
If any location clusters from step 4 were unresolved, ask now — you can see the actual content.
For each near-duplicate group flagged in step 5, recommend the single best image and list the rest as drops. Explain the choice briefly (sharper, better composition, less obstruction, etc.).
The user confirms before anything is finalized.
Final upload set = all Tier 1 + Tier 2 after deduplication.
One step, not three. For each image in the final set:
upload/ with the new name in a single cp operation{{Information}} block into wikimedia_descriptions.txtNo intermediate "move then rename" — the file lands in upload/ with its final name
directly.
After copying to upload/, strip non-photographic metadata embedded by phone apps.
The MOOD: STOCK app embeds junk in several EXIF/XMP fields: title set to
"MOOD: STOCK DIGI-N", UserComment filled with JSON theme config, Description fields
with app labels.
Step A — macOS extended attributes:
for f in upload/*.JPG upload/*.jpg; do
xattr -d com.apple.metadata:kMDItemComment "$f" 2>/dev/null
xattr -d com.apple.metadata:kMDItemDescription "$f" 2>/dev/null
done
Step B — EXIF/XMP fields via exiftool:
exiftool -overwrite_original \
-UserComment= \
-Description= \
-ImageDescription= \
-XMP-dc:Description= \
upload/*.JPG
Run both steps before generating descriptions so there's no confusion about what "description" means. This is mandatory, not optional — files uploaded with app metadata get flagged on Commons.
Pattern: [Subject] [Location] [Year].[ext]
IMG_0326.JPG → Wilder Kaiser massif panorama Tyrol 2026.JPG
IMG_0594.JPG → Naviglio Grande canal Milan 2026.JPG
IMG_0742.JPG → Port Hercule Monaco with yachts 2026.JPG
Before writing any descriptions, discover the real Commons category names for every subject and location in the upload set. Never guess a category name and check if it exists. Instead, search first and pick from results.
Method: Use the Commons API list=search in namespace 14 (categories) with keyword
queries. This returns actual category names with their real capitalization, punctuation,
and disambiguation patterns.
# One loop, all subjects at once. Run this BEFORE writing descriptions.
for q in "Notre-Dame+Garde+Marseille" "Vieux-Port+Marseille" "Cours+Julien+Marseille" \
"Invader+mosaics" "trams+Marseille"; do
echo "=== $q ==="
curl -s "https://commons.wikimedia.org/w/api.php?action=query&list=search\
&srsearch=${q}&srnamespace=14&srlimit=5&format=json" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
for r in data['query']['search']:
print(r['title'])"
done
Why search-first matters: Commons naming is unpredictable. "Notre-Dame de la Garde" does not exist — the real category is "Basilique Notre-Dame de la Garde." "Vieux-Port de Marseille" does not exist — it's "Vieux-Port (Marseille)." "Palais de la Bourse (Marseille)" uses a lowercase 'b' and 'à' instead of parentheses. You will not guess these correctly.
Do not use action=query&titles=Category:Guessed Name as the discovery step. That
only confirms or denies an exact string — useless when the real name differs from your
guess. Reserve exact-title checks for a final validation pass only, after you already
have names from search results.
Collect all verified category names into a lookup list, then reference that list when
writing each {{Information}} block.
Minimum categories: Aim for at least 3 verified categories per image. Use multiple search queries per subject if the first query doesn't yield enough. Search for the subject (species, building, artwork), the specific location (neighborhood, park, museum), and the broader geographic area (city, arrondissement, canton). One real category is better than a guessed one, but most images should hit 3.
Generate wikimedia_descriptions.txt following the exact format in
references/information-template.txt. One {{Information}} block per image, bilingual
descriptions (English + German), {{Taken on}} date template with location, and
categories drawn from the verified lookup list above.
Before any actual upload, run the upload script in dry-run mode so the user can review what will happen.
cd upload/
python upload_to_commons.py --dry-run
This prints each filename, file size, and a description preview. Present the output to the user and wait for explicit confirmation before proceeding to step 9.
This is a hard gate. Do not proceed to actual upload without user confirmation.
Before the first upload, check that a .venv with pywikibot exists in the working
directory. If not, create it:
python3 -m venv .venv
.venv/bin/pip install pywikibot
Pywikibot looks for config files in the current working directory. Since the upload
script runs from upload/, place both config files inside the upload/ directory.
This is the single most common setup error — putting them in the parent directory
will cause a "username undefined" crash.
Check for user-config.py and user-password.py in the upload/ directory. If they
don't exist, generate them there:
user-config.py:
family = 'commons'
mylang = 'commons'
usernames['commons']['commons'] = 'Mike is Michi'
password_file = 'user-password.py'
user-password.py:
('Mike is Michi', BotPassword('commons-upload', 'PASTE_BOT_PASSWORD_HERE'))
For the bot password, check the memory file reference_commons_credentials.md. Never
hardcode credentials into skill files or commit them.
cd upload/
.venv/bin/python upload_to_commons.py --delay 5
The upload script is located at
~/.claude/skills/commons-upload/scripts/upload_to_commons.py. Copy it to the
upload/ directory before running, or invoke it with its full path.
Flags:
--dry-run — preview without uploading--file "pattern1" "pattern2" — upload only matching filenames--delay N — seconds between uploads (default 5)--overwrite — re-upload files that already exist on Commons (useful after
stripping metadata from previously uploaded files)--no-verify — skip post-upload verificationThe script writes upload_log.txt with timestamps, filenames, status, and Commons URLs.
The upload script runs verification automatically after uploads complete (unless
--no-verify was passed). It checks each file via the Commons API to confirm:
Propagation delay: Commons may take 30-60 seconds to index newly uploaded files. The script's built-in verification often runs too early and reports false "MISSING" results. If the script reports all files as missing immediately after a successful upload batch, don't trust it. Instead, wait 30 seconds and run a manual spot-check on 2-3 files via the API:
curl -s "https://commons.wikimedia.org/w/api.php?action=query\
&titles=File:Example+Name.JPG&format=json" | python3 -c "
import sys, json
pages = json.load(sys.stdin)['query']['pages']
for pid, p in pages.items():
print('EXISTS' if int(pid) > 0 else 'MISSING', p['title'])"
If spot-checks confirm existence, the batch is fine. Only investigate individual files that still show as missing after 60+ seconds.
Review upload_log.txt and present a summary: total uploaded, skipped, failed, and
links to the Commons file pages.
The complete end-to-end flow when the user invokes this skill:
1. Resolution check — drop sub-2MP
2. EXIF extraction — flag technical issues
2b. Front-camera filter — flag likely selfies, skip in visual review
3. Format duplicate pruning — keep best version per base name
4. Gather location context — reverse geocode via Nominatim
5. Visual review + tiers — classify (skip selfies + sample personal clusters)
6. Resolve near-duplicates — pick best per group
7. Copy+rename+describe — upload/ folder with descriptions
7a. Strip metadata — exiftool + xattr cleanup
7b. Generate descriptions — wikimedia_descriptions.txt (3+ categories each)
8. Dry-run preview — user reviews before upload
9. Upload to Commons — venv + pywikibot + bot password (config in upload/)
10. Post-upload verification — API check (wait for propagation) + log review
Steps 1-7 produce the upload-ready set. Steps 8-10 handle the actual upload. The user must confirm between step 8 and step 9.
--overwrite flag. The script will re-upload
even if the file already exists on Commons.upload_log.txt for specifics. Common causes: network
timeout, file too large, bot password expired. Re-run with --file "failed_name*" to
retry specific files.36:["$","$L3b",null,{"content":"$3c","frontMatter":{"name":"commons-upload","description":"Evaluate, curate, and prepare images for upload to Wikimedia Commons. Use this skill whenever the user wants to upload photos to Wikimedia Commons, contribute images to Wikipedia, prepare stock photos for Commons, assess image quality for Commons, create Wikimedia description boxes, generate {{Information}} templates, rename images for Commons conventions, or categorize photos for Wikimedia. Also trigger when the user mentions "Commons upload", "Wikimedia", "Commons-tauglich", "Bilder hochladen", "Commons-Beschreibung", or has a batch of images they want to filter and prepare for contribution. This skill covers the full pipeline from raw image directory to upload-ready files with descriptions — not just one step.\n"}}]