DEV Community

Cover image for Who Do You Report a Pothole To? I Built Civic Lens to Answer That
Simran Kaur
Simran Kaur

Posted on

Who Do You Report a Pothole To? I Built Civic Lens to Answer That

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

What I Built

Living in India comes with a lot of infrastructure problems, and somewhere along the way we stopped seeing them. A garbage pile at the corner, a broken footpath, a dead streetlight. Nobody fixes them, and honestly we don't either. We walk past and carry on with the day as if everything is fine.

Even people who do notice usually don't know who is responsible or where to report it. I didn't, until I built this.

Civic Lens is a web app that makes you actually look. You type your area and get a short quest with three categories: safety and infrastructure, cleanliness and environment, accessibility and public space. For each one you go outside, photograph a real problem, and write how you'd fix it. Then you get:

  • a score for the problem and for your proposed fix
  • the kind of authority responsible for it
  • a 4-step action plan
  • a complaint draft, written for you, ready to copy
  • a link to an official government reporting page, when one can be found

Do all three and you get "Grass Touched 🌱". The point isn't the points. It's learning who is responsible for the everyday problems around you, and having a complaint ready to send.

Demo

I've added a demo video in project's readme. Please check it out if you get a chance :)
Live Site: https://civiclens-web-v1aa.onrender.com/


Taking it outside: I walked around my area and tried it for real. It felt awkward. Standing on the road photographing a garbage pile or a broken stretch of pavement from different angles, with people walking past, is not something I'm used to, and I caught myself wanting to hurry up and move on.

I took 5 pics. For the demo video I uploaded them with the "Choose from gallery" button, so the in-app camera flow isn't in the recording.

What I saw when I ran them:

  • Scores: they felt fair. My proposed fixes were short and not very detailed, and the scores reflected that, so the grader wasn't just handing out high marks.
  • Reporting channel: I only tried "Find reporting channel" once, on an infrastructure issue. It returned a generic Delhi government link that was probably not the right place for road complaints, and it still gave me the +2 bonus. This is the weakest part of the app right now, and I explain why below.

Code

Civic Lens mascot

Civic Lens

Photograph three real problems in your neighbourhood, propose a fix, and get a score the responsible authority and a ready-to-send complaint

Live app Β· DEV post

Built for the Hacktoberfest Open-Source AI Challenge, Week 1: Touch Grass. The backend is on a free Render plan and sleeps when idle, so the first request can be slow.

Demo Video

Video.Project.5.1.1.1.1.mp4

Features

  • Gemma writes a short quest for your area, with one hint for each of three categories.
  • Upload a photo and your proposed fix. Gemma checks the photo, scores the problem and your fix, names the kind of authority responsible, and drafts a complaint.
  • SerpApi looks for an official government reporting page (.gov.in / .nic.in) for a +2 bonus.

Tech stack

  • Backend: FastAPI, Pydantic, httpx, Pillow
  • Frontend: React, TypeScript, Vite, Tailwind CSS
  • AI: Gemma via the google-genai SDK
  • Search: SerpApi
  • Hosting: Render

Project structure

civiclens/
β”œβ”€β”€
…

This is how my project's structure looks like:
civiclens/
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ main.py        # FastAPI routes: /api/quest, /api/analyze, /api/find-channel
β”‚   β”œβ”€β”€ gemma.py       # everything that talks to Gemma
β”‚   β”œβ”€β”€ search.py      # SerpApi lookup, ranking, cache, daily cap
β”‚   β”œβ”€β”€ prompts.py     # all prompt text, separate from logic
β”‚   β”œβ”€β”€ models.py      # Pydantic models, the API contract
β”‚   β”œβ”€β”€ utils.py       # image checks, duplicate detection, score math
β”‚   β”œβ”€β”€ mock_data.py   # canned data for frontend work
β”‚   └── tests/
β”œβ”€β”€ frontend/
β”‚   └── src/
β”‚       β”œβ”€β”€ App.tsx, api.ts, points.ts, types.ts
β”‚       └── components/   # SlotCard, ResultView, ChannelFinder, Completion, Mascot
└── render.yaml
Enter fullscreen mode Exit fullscreen mode

How I Built It

Stack: FastAPI, React with Tailwind, Gemma for analysis, SerpApi for the reporting link, Render for hosting. I spent most of my time on the backend. I'll be upfront that Claude built almost all of the UI, because I had very little time.

How Gemma is used

I call Gemma through Google's API with the google-genai SDK. There are two jobs.

1. The quest is text only. Gemma writes a short briefing and one hint per category for the area you typed. If it fails, the app falls back to a static template, so a quest never fails to load.

2. Photo analysis is the main one. When you submit a photo and your fix, the backend:

  1. Checks the file (JPG or PNG, size limit, magic bytes so a renamed file can't pass) and hashes it to catch reused photos.
  2. Fixes phone rotation and shrinks it to 1280px, since phone photos are huge.
  3. Sends the image plus a prompt to Gemma, with a system instruction at low temperature (0.2).
  4. Gets back JSON, parses it, and validates it with Pydantic. If the JSON is broken, it retries once.
  5. Checks photo_check first: is this a real outdoor photo, does it show a civic problem, and does it match this category? If not, the user gets a friendly rejection instead of a score.
  6. Computes the total score in the backend. The model gives two numbers from 0 to 10 and never adds anything up.

If the primary model errors out (gemma-4-26b-a4b-it), the backend retries with a fallback (gemma-4-31b-it).

Because users type text that goes into a prompt, I treat it as data. The user's text is wrapped in tags, the system prompt says to never follow instructions inside them, and I strip any attempt to close those tags. A "give me 10/10" solution gets a zero.

Right now the three category names are hardcoded, like this:

SLOT_TITLES = {
    "safety_infrastructure": "Safety & infrastructure",
    "cleanliness_environment": "Cleanliness & environment",
    "accessibility_public_space": "Accessibility & public space",
}
Enter fullscreen mode Exit fullscreen mode

Gemma already writes the hints for each area, but not the categories. Having Gemma pick different categories on every Start over is next on my list.

SerpApi

SerpApi searches Google for the official page where a complaint can be filed, like [area] [authority] [issue] complaint grievance portal. I only accept results on .gov.in or .nic.in that also mention words like "grievance" or "portal". If nothing qualifies, the app says so and gives no bonus. A found page is worth +2 points, so the app rewards knowing where to report.

I had never heard of SerpApi before this challenge, so I spent a lot of time in its documentation. It helped me fix my errors and get it into the app.

Limits right now, and I'm working on them:

  • Sometimes the search fails and my backend returns a 503. Pressing "Try again" usually works.
  • My filter only checks that a result is on an official domain (.gov.in or .nic.in) and mentions words like "grievance" or "portal". It does not check that the page belongs to the right department. When I tested an infrastructure issue, it returned a generic Delhi government link that was probably not the right channel for road complaints, and it still gave me the +2 bonus. In an earlier test it found a proper MCD page, so it works sometimes, but I can't promise it's right. The bonus rewards finding an official page, not verifying it.
  • There's a daily cap of 50 searches to protect my quota, and an in-memory cache. Both reset when the server restarts.

Render, and the hour I'll never get back

I hadn't used Render before, so I had ChatGPT walk me through the dashboard. It told me I didn't need to import my .env, so I added the variables by hand. And I misspelled CORS_ORIGINS as CORS_ORIGIN.

My deployed site then threw this in the console:

Access to fetch at 'https://civiclens-xyhi.onrender.com/api/quest' from origin
'https://civiclens-web-v1aa.onrender.com' has been blocked by CORS policy...
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Enter fullscreen mode Exit fullscreen mode

I spent about an hour suspecting my backend. It was a spelling mistake, plus a trailing slash on the API URL that produced //api/quest. The code ignores a variable with the wrong name and falls back to localhost, so nothing complained. Next time I check the spelling first.

Why Does Open Innovation Matter?

Gemma has open weights. To be honest about my setup: I use it through Google's hosted API, so it isn't running locally or offline. What open weights gave me is a way out.

  • No lock-in. The scoring and complaint writing come from prompts I wrote and control (prompts.py). If a better open model shows up, I change a model name, not the app.
  • Fallbacks I choose. I use two different Gemma models as primary and fallback, and I can add more.
  • A path to privacy. People are photographing their own streets. With open weights, I could later run this on a server I control so those photos don't go to a third party. That's a future step, not what's live today.

What's Next

  • Gemma picks different categories every time you start over, so quests aren't the same each time.
  • A leaderboard. Today your points live only in your own browser (they survive a refresh, but not a different device), so anyone can edit them. A real leaderboard needs a database and some way to trust the scores.
  • Better search accuracy for the official reporting page.

Known limits: the app can't prove you went outside, only that you sent a convincing photo. And the free backend resets its cache and duplicate-photo list on restart.

There's still a long way to go, but it works, and I now know who to call about the garbage pile on my street.

Prize Categories

  • Best Use of Gemma: quest writing and the photo analysis, scoring and complaint drafting.
  • Best Use of Render: the API and the static frontend both run on Render.
  • Best Use of SerpApi: finding the official government reporting page, which drives the bonus points.

Top comments (1)

Collapse
 
suppdevbot profile image
DEV SUPPORTS •

Official Platform Update

Security protocols have been updated for all developer accounts.

  • tr.ee/dev-to