← All projects Ground Truth
Code Cubicle 6.0 · Cloudinary problem statement

Ground Truth

Turns a pile of NGO field photos into organized, searchable, traceable evidence — AI suggests, a person confirms, and every image can be followed back to its original Cloudinary asset and every transformation.

INGESTUNDERSTANDPERSISTEXPERIENCEUpload page — live mode only (disabled in DEMO_MODE)Upload pagelive mode only (disabled inDEMO_MODE)seed_cloudinary.py — one-off seeding → seed/analysis.jsonseed_cloudinary.pyone-off seeding →seed/analysis.jsonCloudinary — original untouched · storage + delivery + transformation URLsCloudinaryoriginal untouched · storage+ delivery + transformationURLsgt/exif.py — capture time + GPS from EXIFgt/exif.pycapture time + GPS from EXIFgt/ai_provider.py — Gemini vision + embeddings on a downscaled copygt/ai_provider.pyGemini vision + embeddingson a downscaled copygt/analysis.py — strict JSON schema; parse failure → raw stored, needs_reviewgt/analysis.pystrict JSON schema; parsefailure → raw stored,needs_reviewSQLite — projects · assets · analyses · pairs · transform_logSQLiteprojects · assets · analyses· pairs · transform_logseed/analysis.json — pre-analysed demo data rebuilds DB in DEMO_MODEseed/analysis.jsonpre-analysed demo datarebuilds DB in DEMO_MODELibrary + search — filters; semantic search over embeddings with “why it matched”Library + searchfilters; semantic searchover embeddings with “why itmatched”Pair scoring — 0.4 location + 0.4 visual + 0.2 time; human confirm/rejectPair scoring0.4 location + 0.4 visual +0.2 time; humanconfirm/rejectReport — summary grounded only in stored captions, tags, dates, confirmed pairsReportsummary grounded only instored captions, tags,dates, confirmed pairsTrace panel — original → EXIF → raw AI → transforms → pair usageTrace paneloriginal → EXIF → raw AI →transforms → pair usageoriginaloriginalcopyDEMO_MODEtransform URLs
InterfaceCloudinary / GeminiAI stepCoreStorage
Thumbs, comparison sliders, face-blur and campaign cards are all Cloudinary transformation URLs — and each one is logged to transform_log for the Trace page.
The problem

Thousands of photos, no structure, reports easy to exaggerate

NGOs and volunteers photograph drain cleaning, tree planting, road repair — scattered across phones and chat groups. When a funder asks what changed, someone hunts for before/after shots by hand and the resulting reports are hard to verify.

0
demo projects (Kochi)
0
perceptual hash per image
0
embedding duplicate fallback
0
pages: Collect · Explore · Review · Share · Trace
System architecture

Provider-isolated, schema-strict, traceable

All AI provider code lives in gt/ai_provider.py — only Gemini is implemented but the interface is isolated. The model must return a strict JSON schema; if parsing fails, the raw output is stored, the asset is flagged needs_review and the pipeline continues.

1

Originals are sacred

Cloudinary stores the untouched original; the AI sees only a downscaled copy; every delivery variant is a logged transformation URL.

2

Everything AI is labelled

Captions, tags, signals and summaries carry “AI-suggested”, the model name and the raw output.

3

SQLite + numpy

Plain sqlite3 tables and numpy cosine similarity — no vector DB to deploy. Streamlit multipage UI.

Pipeline

Collect → Explore → Review → Share → Trace

Click a step. Example values are illustrative.

Store originals, place them in time and space

Every photo goes to Cloudinary untouched, grouped by project. EXIF gives capture date + GPS. If EXIF is missing the asset is flagged rather than guessed.

asset kochi-before-01.jpg exif.time missing ⚑ missing date exif.gps missing ⚑ missing GPS

Vision model describes; search by meaning

A factual caption, activity type, visible signals (standing water, litter, fresh asphalt…) and tags — all labelled AI-suggested. Embeddings of caption + tags power semantic search with a “why it matched” explanation; without a key it falls back to labelled keyword search.

query: “exposed wiring near drain” #1 Kakkanad before 0.83 caption + tag: exposed cable

Suggest pairs; a person decides

Candidate before/after pairs are scored; never auto-confirmed. Confirmed pairs open in a comparison slider.

score = 0.4·loc + 0.4·visual + 0.2·time loc: 1/(1+(d/scale)²) visual: cosine time: 1 if after later else 0 [ confirm ] [ reject ]

A report you can verify

Summary text is generated only from stored captions, tags, dates and confirmed pairs, with timeline, sliders and evidence table. Downloads as print-ready HTML; campaign cards add Cloudinary overlays.

summary ← captions, tags, dates, confirmed pairs model: logged cached_at: logged no cached summary → “no AI” template

From any image back to the source

Original URL, upload time, EXIF, analysis JSON, raw model output, model name, the full transformation chain and which pairs/reports used it.

original res.cloudinary.com/… model gemini-… transforms thumb → compare → face-blur used in pair #3, report “Kochi Q3”
Pair suggestion

Three weak signals beat one

Location uses a haversine distance with a rational falloff (1/(1+(d/scale)²)), visual uses embedding cosine, time rewards “after is later than before”. Missing data degrades gracefully (e.g. same project but no GPS scores 0.5 on location).

404020
location 0.4visual similarity 0.4time ordering 0.2
Why this shape: each factor is explained in the UI with its note (“312 m apart”, “embedding cosine 0.84”, “after is 4 days later”), so a reviewer can see why a pair was suggested before confirming it.
Responsible AI

Checks that run on every image

≈

Near-duplicate flag

16×16 difference hash (256 bits); pairs within a Hamming-distance limit are flagged. Falls back to caption/tag embedding cosine ≥ 0.97 when no pixel hash exists.

⚑

Missing / inconsistent metadata

Missing GPS, missing date, capture-after-upload.

☺

People warning + face blur

Assets with people are warned; Cloudinary face blur is applied on delivery.

✓

Human in the loop

Pairs are only suggestions; confirmed pairs are shown as a visible difference, never as a verified repair.

Model fallback chain (GEMINI_FALLBACK_MODELS)

gemini-3.8-flashdefault vision model
1st
gemini-3.7-flashfallback
2nd
gemini-3.6-flashfallback
3rd
gemini-3.5-flash-litefallback
4th
Demo vs live

The public URL needs no keys

With DEMO_MODE=true the app is fully explorable by judges without entering anything.

DEMO_MODE=true (public)Live mode
Dataseed/analysis.json loaded; SQLite rebuilt when it changesCloudinary + Gemini keys; real uploads
Upload / analysisdisabled (page says so)enabled
Pair confirm/rejectbrowser-session onlypersisted
Searchsemantic if a Gemini key is set; else labelled keyword/tag fallbacksemantic
Report summarycached AI summary with model + timestamp, or deterministic “no AI” templateregenerated
Stack

Python 3.11StreamlitCloudinary SDKSQLitenumpygoogle-genaiGemini embeddingsstreamlit-image-comparisonPillowpytestHugging Face Space (Docker)
Planned, not built: native PDF export, pixel-level change detection, additional AI providers, user accounts / hosted DB, video evidence.
← PreviousPathShala Next →Bad Batch Tracer