A local web app for tracing where a photo appears on the internet.
Upload an image or paste a URL. The app calls Google Cloud Vision Web Detection to find exact-file matches, then enriches those matches with scraped page metadata, EXIF data, and oldest-known archive dates from the Wayback Machine.
The frontend shows the full pipeline step-by-step -- raw JSON, normalized candidates, ranking decisions, and errors -- so you can inspect every part of the trace, not just the final answer.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Reserve Image Search β
β β
βββββββββββββββ β ββββββββββββ ββββββββββββββββ β
β Browser β β β FastAPI β β SQLite β β
β React + βββHTTPββββββ>β β Backend β<ββββ>β backend/ β β
β Vite UI β<ββJSONβββββββ β β β photos.db β β
β (port β β β (port β ββββββββββββββββ β
β 5173) β β β 8000) β β
βββββββββββββββ β ββββββ¬ββββββ ββββββββββββββββ β
β β β uploads/ β β
β β β (local β β
βββββββββΌβββββββββββββ files) ββββββββββββββββββ
β ββββββββββββββββ
β
βββββββββββββββββββΌβββββββββββββββββββ
β β β
v v v
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Google Cloud β β Metadata β β Wayback β
β Vision API β β Scraper β β Machine CDX β
β β β β β β
β - full match β β - Open Graph β β - oldest β
β - page match β β - Twitter β β archive β
β - web entity β β - JSON-LD β β timestamp β
β - labels β β - EXIF β β β
ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββ¬ββββββββ
β β β
v v v
ββββββββββββββββββββββββββββββββββββββββββββββββ
β Ranking Engine β
β β
β Sorts by: Wayback > Publish > Modified > β
β EXIF. Prefers exact over partial matches. β
β Selects best_source with metadata. β
ββββββββββββββββββββββββββββββββββββββββββββββββ
Each trace job runs through five stages. The frontend polls for status and shows each stage as it completes.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Trace Pipeline β
β β
β 1. IMAGE SEARCH β
β β Send image to Google Cloud Vision Web Detection β
β β Optional: also query SerpAPI Google Lens β
β β Returns: exact matches, page candidates, web entities, labels β
β v β
β 2. CANDIDATE SELECTION β
β β Filter and prioritize results β
β β Keep fullMatchingImages as exact-file evidence β
β β Select up to 20 pages for scraping β
β v β
β 3. METADATA SCRAPING β
β β Fetch candidate pages and extract metadata β
β β Sources: Open Graph, Twitter Card, JSON-LD, β
β β Dublin Core, HTML meta tags, EXIF, HTTP headers β
β β Runs up to 6 concurrent workers β
β v β
β 4. WAYBACK ENRICHMENT β
β β Query Wayback Machine CDX API for each result β
β β Find the oldest known archived snapshot β
β v β
β 5. RANKING β
β β Sort results by oldest evidence β
β β Prefer: Wayback > Publish Date > Modified Date > EXIF β
β β Select best_source with full metadata β
β v β
β DONE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PENDING
β
v
PROCESSING_VISION
β
v
SELECTING_EXACT_MATCHES
β
v
SCRAPING_METADATA
β
v
CHECKING_WAYBACK
β
v
RANKING_RESULTS
β
v
COMPLETED ERROR
This app is intentionally conservative about what counts as a match:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β fullMatchingImages -> EXACT image file evidence β
β (same file hash) β
β β
β pagesWithMatchingImages -> CANDIDATE page evidence β
β (may be context/embed) β
β β β
β βββ scraper confirms the exact image URL is on the page β
β β -> upgraded to EXACT page β
β β β
β βββ cannot confirm β
β -> stays as fallback candidate β
β β
β visuallySimilarImages -> stored in raw JSON only β
β (not used for ranking) β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
photo-detector/
βββ backend/
β βββ app/
β β βββ api/
β β β βββ routes.py # REST API endpoints
β β βββ db/
β β β βββ database.py # SQLite connection setup
β β β βββ models.py # SQLAlchemy models (photo_jobs, bulk_jobs)
β β βββ services/
β β β βββ pipeline.py # Main pipeline orchestrator
β β β βββ bulk_pipeline.py # Bulk processing orchestrator
β β β βββ scraper.py # Page metadata extraction
β β β βββ vision_client.py # Google Vision API client
β β β βββ serpapi_client.py # SerpAPI Google Lens client
β β β βββ utils.py # Helpers
β β βββ main.py # FastAPI app entry point
β βββ uploads/ # Uploaded image files (local)
β βββ photos.db # SQLite database
β βββ .env # Environment variables (API keys)
β βββ requirements.txt # Python dependencies
βββ frontend/
β βββ src/
β β βββ components/
β β β βββ UploadBox.tsx # Single image upload / URL input
β β β βββ ResultsView.tsx # Trace results with step timeline
β β β βββ BulkView.tsx # Bulk job management
β β β βββ BulkInput.tsx # Bulk URL input form
β β βββ services/
β β β βββ api.ts # Frontend HTTP client (Axios)
β β βββ hooks/ # Custom React hooks
β β βββ App.tsx # Main app component
β β βββ main.tsx # React entry point
β βββ package.json # Node.js dependencies
β βββ vite.config.ts # Vite build config
βββ launcher.bat # One-click Windows launcher
βββ README.md
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Required β
β β
β Python 3.12+ https://python.org β
β Node.js 20+ https://nodejs.org β
β Google Cloud project with Vision API enabled β
β -> Get a service account key or API key β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Optional β
β β
β SerpAPI account β
β -> Enables Google Lens as an additional β
β search mode (serpapi / combined) β
β -> https://serpapi.com β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
# Backend
cd backend
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txt
# Frontend
cd frontend
npm installCreate backend/.env:
# Required: Google Cloud Vision
GOOGLE_APPLICATION_CREDENTIALS=C:\path\to\service-account.json
# -- or --
GOOGLE_VISION_API_KEY=your_api_key_here
# Optional: SerpAPI (enables additional search mode)
SERPAPI_API_KEY=your_serpapi_key_hereAuth modes explained:
ββββββββββββββββββββββββββββββββββββββββ
β GOOGLE_APPLICATION_CREDENTIALS β
β (Recommended) β
β β
β Point to a service account JSON β
β file. Used by Google's client β
β library via Application Default β
β Credentials. β
β β
β Smallest permissions + protect β
β the key file. β
ββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββ
β GOOGLE_VISION_API_KEY β
β (Fallback) β
β β
β Works but may hit 403 / β
β API_KEY_SERVICE_BLOCKED if your β
β API key has restrictions. β
ββββββββββββββββββββββββββββββββββββββββ
Windows (one click):
launcher.batThis opens two terminal windows -- backend on port 8000, frontend on port 5173.
Manual:
# Terminal 1: Backend
cd backend
.venv\Scripts\activate
uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
# Terminal 2: Frontend
cd frontend
npm run devThen open http://localhost:5173.
POST /api/trace
Body: multipart file as "file"
or form field "image_url"
Query: search_mode=google_vision | serpapi | combined
Response: { "job_id": "uuid", "status": "PENDING" }
GET /api/jobs/{job_id}
Response: { status, error_message, results: { ... full pipeline JSON } }
POST /api/bulk/trace
Body: JSON { "urls": [...], "search_mode": "..." }
Response: { "bulk_job_id": "uuid", "status": "PENDING" }
GET /api/bulk/{bulk_job_id} # status + per-job results
GET /api/bulk/{bulk_job_id}/export # CSV download
GET /api/jobs/recent # list recent trace jobs
{
"query": "input image path or URL",
"source_kind": "local_file",
"pipeline_run_at": "2026-04-14T15:59:37Z",
"steps": [],
"vision": {
"auth": {},
"request": {},
"rawResponse": {},
"webDetection": {}
},
"candidates": {
"exactImageUrls": [],
"pageCandidates": [],
"selectedPageUrls": [],
"directExactImages": [],
"confirmedExactPages": [],
"fallbackCandidates": []
},
"scraped_pages": [],
"exact_matches": [],
"fallback_candidates": [],
"best_source": {},
"ranking": {},
"errors": []
} βββββββββββββββββββββββββββββββββββββββββββ
β From pages β
β β
β Open Graph (og:image, og:title, ...) β
β Twitter Card (twitter:card, ...) β
β JSON-LD (ImageObject, Article, ...) β
β Dublin Core (DC.title, DC.creator) β
β HTML meta tags (author, date, ...) β
β Platform-specific heuristics β
β β
βββββββββββββββββββββββββββββββββββββββββββ€
β From images β
β β
β HTTP headers (Last-Modified, ...) β
β EXIF data (DateTime, Artist, ...) β
β β
βββββββββββββββββββββββββββββββββββββββββββ€
β From external archives β
β β
β Wayback Machine oldest snapshot β
β β
βββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β google_vision (default) β
β Uses Google Cloud Vision Web Detection only. β
β β
β serpapi β
β Uses SerpAPI Google Lens only. β
β Requires SERPAPI_API_KEY in .env. β
β β
β combined β
β Runs both and merges results. β
β Requires both API keys. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Paid API:
- Google Cloud Vision Web Detection
- First 1,000 units/month: free
- 1,001 - 5,000,000 units/month: USD 3.50 per 1,000 units
- Always verify: https://cloud.google.com/vision/pricing
Paid API (optional):
- SerpAPI: pay-per-request, see https://serpapi.com/pricing
Everything else is local:
- SQLite, React/Vite frontend, scraping, Wayback CDX queries all run on your machine with no separate billing.
- Oldest Wayback snapshot is not the same as proven first upload.
- Many platforms strip EXIF data from images.
- Some pages block scraping, lazy-load content, or require JavaScript.
- Google Vision may return context pages that don't expose enough HTML to prove they serve the same file.
- Some sites remove
Last-Modifiedheaders or normalize images behind CDNs. - This app is useful for investigation -- it does not prove legal authorship by itself.
- Google Cloud Vision Web Detection: https://docs.cloud.google.com/vision/docs/detecting-web
- Google Cloud Vision pricing: https://cloud.google.com/vision/pricing
- Google Cloud auth setup: https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment
- Internet Archive Wayback Machine: https://archive.org/web/