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.
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.
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.
Cloudinary stores the untouched original; the AI sees only a downscaled copy; every delivery variant is a logged transformation URL.
Captions, tags, signals and summaries carry “AI-suggested”, the model name and the raw output.
Plain sqlite3 tables and numpy cosine similarity — no vector DB to deploy. Streamlit multipage UI.
Click a step. Example values are illustrative.
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.
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.
Candidate before/after pairs are scored; never auto-confirmed. Confirmed pairs open in a comparison slider.
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.
Original URL, upload time, EXIF, analysis JSON, raw model output, model name, the full transformation chain and which pairs/reports used it.
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).
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 GPS, missing date, capture-after-upload.
Assets with people are warned; Cloudinary face blur is applied on delivery.
Pairs are only suggestions; confirmed pairs are shown as a visible difference, never as a verified repair.
GEMINI_FALLBACK_MODELS)With DEMO_MODE=true the app is fully explorable by judges without entering anything.
| DEMO_MODE=true (public) | Live mode | |
|---|---|---|
| Data | seed/analysis.json loaded; SQLite rebuilt when it changes | Cloudinary + Gemini keys; real uploads |
| Upload / analysis | disabled (page says so) | enabled |
| Pair confirm/reject | browser-session only | persisted |
| Search | semantic if a Gemini key is set; else labelled keyword/tag fallback | semantic |
| Report summary | cached AI summary with model + timestamp, or deterministic “no AI” template | regenerated |