跳到正文
DEV Community· Asuran·· 4 小時前AI 評分51

Gemma 讀對泊車標誌,卻判錯規則

Gemma read the parking sign perfectly. Then it got the rule wrong.

AI 導讀

作者開發 TicketBack,讓本地 Gemma 3 4B 讀取泊車告票和標誌,再由程式規則判斷是否有上訴理由,並根據用戶確認的資料草擬信件。以合成照片進行的評測中,告票欄位有 164/168 項讀取正確(98%),12 張中有 9 張所有欄位均正確,標誌檢查則為 40/40(100%)。照片、車牌和信件均留在用戶電腦;作者指出,工具不包含城市法規判斷,也不是法律意見。

正文

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

What I Built

TicketBack turns two phone photos into a parking ticket appeal: one of the ticket and one of the sign you parked under. A local open-weight model reads both. Plain code decides whether you have a case. If you do, the model writes the letter from facts you confirmed. Nothing leaves your computer.

The problem is small and very common. A $65 ticket is too cheap to pay anyone to fight and just expensive enough to ruin a day. Paying is easier than fighting, even when the sign says the restriction ended an hour before the ticket was written. Fighting it means reading the ticket closely, working out the sign's hours and days, then writing a letter that sounds firm without sounding angry. Ten minutes of fiddly work that is easy to skip.

Who pays: drivers at a few dollars per appeal. Also small fleets (delivery vans, plumbers, contractors) that can collect tickets every month and have nobody whose job it is to contest them. The pitch to a fleet is simple: photograph each ticket when it arrives and get a letter for the ones worth fighting. Because the model runs locally, an appeal has no API cost to pass on, so a low price still works.

Demo

The real app, after reading a synthetic ticket and sign (screenshots from a headless run of the actual flow):

1. You check what the model read. Every field is editable. Blanks are outlined, so nothing runs on a value you have not seen.

Fields read from the ticket photo

2. Rules find the grounds. This ticket was written on a Wednesday at 19:42 under a sign that says 8 AM to 6 PM, Monday to Friday.

One strong ground for appeal, with the exact facts it used

3. Gemma writes the letter. The header, reference line and sign-off are code. Every number in the body is checked against your facts.

The drafted appeal letter

Change the time to 10:15 and press the button again: no ground is found. TicketBack says so instead of writing a weak letter. That refusal matters as much as the letter.

Run it yourself in about a minute (needs Ollama):

ollama pull gemma3:4b
git clone https://github.com/zkasuran/ticketback && cd ticketback
uv sync && uv run ticketback   # http://127.0.0.1:8765

The repo ships the test photos, so you can try it without a real ticket.

Code

GitHub logo zkasuran / ticketback

Appeal a parking ticket from two photos. Local Gemma reads them, plain code decides, nothing leaves your computer.

TicketBack: photograph the ticket and the sign, get the appeal. Gemma 3 on your own computer.

Try it · How it works · Accuracy · Security · Real vs not

tests model tests pip-audit model licence

TicketBack turns two phone photos, the parking ticket and the sign you parked under, into an appeal letter. A local open-weight model (Gemma 3 4B through Ollama) reads both photos. Fixed rules, not the model, decide whether you have a ground to appeal. The model then writes the letter from facts you confirmed. Every photo, plate and letter stays on your computer.

Why it exists: a $65 ticket is too small to pay anyone to fight and just big enough to resent. Most people pay it even when the sign says the rule had ended an hour before the ticket.

TicketBack after reading a ticket and a sign: the extracted fields, one strong ground for appeal and the drafted letter

Try it in 60 seconds

ollama pull gemma3:4b          # once, about 3.3 GB
git clone https://github.com/zkasuran/ticketback && cd ticketback
uv sync && uv run ticketback   # serves http://127.0.0.1:8765
Step Do this You will see
1 Pick

…

How I Built It

The stack is small on purpose: Gemma 3 4B (open weights) through Ollama, a FastAPI server, one HTML page with no framework. It runs on a CPU laptop.

The interesting part is what I stopped asking the model to do.

Surprise one: the schema changed what it could see. My first version gave Gemma a JSON schema where every field was optional ("null if not visible"). On a clean, readable ticket it returned 8 of 14 fields and took 74 seconds. Same image, same temperature, but with every field a plain string and the instruction "empty string if not printed": 14 of 14 in 29 seconds. Given permission to skip, a small model skips. Asked to fill every slot, it reads the whole ticket.

Surprise two: perfect reading, wrong meaning. On a sign reading 2 HOUR / PARKING / 9 AM - 7 PM / EXCEPT SUNDAY, Gemma transcribed every word correctly. Then it reported that the rule applies seven days a week. It read "except Sunday" and still put Sunday in the list.

That one bug set the architecture:

  1. The model only copies. It transcribes the ticket into fields and the sign into its words. Nothing else.
  2. Code reads the meaning. A small tested parser turns the transcribed sign text into days, hours and time limits. "Mon thru Fri", "except Sunday", "10 PM - 6 AM" and "anytime" each have a test.
  3. You confirm the facts. A misread plate is a two-second fix when you can see it. It is a disaster when it is buried in a letter.
  4. Rules decide. Nine kinds of ground, each a plain comparison: the ticket time against the sign's window (including overnight windows), the plate on the ticket against yours, the paid-until time against the issue time. Each finding lists the exact values it used. An unreadable sign time never counts as "not in effect", because unknown is not the same as off.
  5. The model writes, code checks. Gemma writes the body from the selected findings only. Code adds the header and sign-off, then flags any number in the body that is not in the confirmed facts.

Another bug made step 4 stricter. An early letter said the restriction "does not apply on Wednesday" for a ticket written on a Wednesday evening. The real reason was the hour, not the day. Now the rule states the exact reason ("the restriction had already ended; it ends at 18:00") and the model has nothing left to guess.

How well does it read? I wrote an eval that renders random invented tickets in three layouts (table, stacked, thermal receipt) with tilt, blur, uneven light and heavy JPEG compression, then scores the model field by field. On 12 tickets and 10 signs:

Measure Result
Ticket fields read exactly 164 of 168 (98%)
Tickets with every field right 9 of 12
Sign checks right (restriction, days, hours, limit) 40 of 40 (100%)
Median time per photo on CPU 96 s ticket, 6 s sign

Times are from a laptop CPU with no GPU, which was busy with other work. On the first run, under heavier load, a sign took about 65 s.

The eval caught a third surprise of the same kind as the first two. On the first full run, the issue time was right on only 6/12 tickets. The issue time is the field the strongest ground depends on. None of the misses were misreads. On tickets printed with "1025 HRS" the model copied the time correctly and my parser rejected it. On 12-hour tickets I had asked the model to convert to 24-hour time. It turned "2:30 PM" into 2:30 and "6:35 PM" into 06:35. So the model now copies the time exactly as printed and code converts it. After that fix: 12/12. Same pattern every time. Each fix took a decision away from the model.

The four fields still wrong are all on the small-print thermal receipt layout: two plates (2VVX292 read as 2VXV292, 2SXN163 as 25XN163), one ticket number with a dropped digit and one violation with an extra word. A misread plate is the dangerous one, because it would make the rules report a false "wrong plate" ground. That is why step 3 is you checking every value. It is also why that finding shows both plates side by side before it goes anywhere near a letter.

The rest is the boring part that makes it trustworthy: 87 tests (boundaries like 18:00 exactly, a second sign that covers the time, a plate typed with dashes), 4 tests against the real model, a strict content security policy with no inline script, a 15 MB upload cap and a release gate that runs everything and prints ALL GREEN.

Why Does Open Innovation Matter?

For this product, running the model yourself is the whole point.

Privacy. A parking ticket ties your plate to a street at a minute of a day. The sign photo shows where you were standing. That is location history. With local Gemma, the photos go from your camera to your own disk and stop there.

Cost. An appeal is worth a few dollars to the person sending it. A per-image API bill eats that margin. A fleet sending dozens of tickets a month would notice. Local inference has no per-ticket fee, so the price can stay low.

Control. The two surprises above were only fixable because I could test the model myself as often as I liked, at temperature 0, with no rate limit and no cost per experiment. I could rerun the same image as often as I liked while changing one thing. That loop is how I found that a schema change was worth six fields. The model is one environment variable (TICKETBACK_MODEL), so a better open vision model next month is a one-line change.

Offline. It works in a car park with no signal, which is where you are when you find the ticket.

Where a closed model would do better: raw reading accuracy on a crumpled, rain-soaked ticket. A bigger model would make fewer mistakes. But the design does not depend on the model being right. It depends on you seeing every value and on code making every decision. That design works just as well with a 4B model on a laptop.

What it does not do: it knows no city's law. The rules compare facts, so a true ground can still lose where a local rule says otherwise. It is a drafting tool, not legal advice. The accuracy numbers come from synthetic photos, not real tickets. Those limits are written into the app and the README too.

Prize Categories

Best Use of Gemma. Gemma 3 4B does both jobs that need a model: reading the ticket and sign photos (vision) and writing the letter. It runs locally through Ollama. The design is built around what a small open model is good at (copying) and what it is not (deciding).


Built with AI assistance (Claude). The design, the tests and every measurement above were run and checked by me. Source is available under a source-available licence; Gemma is used under the Gemma Terms of Use.

來源:DEV Community · dev.to