跳到正文
DEV Community· Fahad Irshad·· 3 小時前AI 評分52

Verso 照片應用缺資料時提問,不替人物猜測姓名

My photo app refuses to name the people in the picture

AI 導讀

Fahad Irshad 公開 Verso,一款以本機 Gemma 4 E2B 根據五項用戶資料撰寫照片標題和描述的 Android 應用,缺資料時會提問而不猜補。

正文

This is my submission for the Hacktoberfest Open-Source AI Challenge, Week 4: Ghost in the Machine.

Twenty invented photographs. Nine of them were missing a fact. The app asked nine questions and invented no names.

A photograph is a haunting with the sound off. Someone is standing in a doorway in 1958, squinting into the sun, holding something they were proud of. The picture survives. The name does not. Neither does the year, or why anyone kept it.

The back of the photograph used to hold that. Someone wrote it down in biro: Nan and Uncle Tom, Empress Street, the morning we moved in. Then the photo was scanned, the back was lost, and what is left is a machine that will now caption it — confidently — as "a vintage family gathering in the 1950s". A sentence with the shape of memory and none of its contents.

Verso writes the back of the photo. You answer five questions — who, where, when, what was happening, why keep it. A local Gemma 4 model writes a title and one sentence. And when one of the five is blank, it does the only honest thing: it writes the question to ask someone.

The back of the photo

What I Built

One screen, one card at a time, set like a card catalog because that is what it is. You pick a photo, answer five short questions, and tap Write the back.

The model returns exactly three lines: a title of seven words or fewer, one sentence in the past tense, and — if a fact is missing — an ask, in the words a person would say out loud. The who, where and when you typed are printed underneath, verbatim. The card can be saved as a printable 4×6 sheet.

The app holds no internet permission. The photo, the answers, the model and the writing all stay on the phone. The model file is the only thing you ever download, and you hand it over yourself.

Demo

Two cards, written on an Android 15 emulator with Gemma 4 E2B running through LiteRT-LM. The photographs are AI-generated stand-ins — I am not putting my family's faces in a public post.

Card one has a hole in it. I answered where and when, left who blank, and told the model the photo is the only one of the two of them as children.

The form

It came back with a title, a sentence, and a question for the person who can still answer it:

A card with a gap

Card two is complete. Same five questions, all answered — and no ask line at all, because there is nothing to ask.

A complete card

Both save as a real PDF page, 4 × 6 inches, which is the point: a family photo archive should be printable at a chemist's counter for the price of a print.

The printed gap

The shelf keeps both kinds of card and says so in the header: 2 cards · 1 with no name on it.

The shelf

The last row on the shelf — "Calibration · 20 invented photos" — is the test harness I described below. It only exists in a debug build; the screenshots in this post come from a debug build because the signed release is arm64 and my emulator is x86_64.

The Part I'm Proud Of: not known Is a Feature

Every captioning tool I have used has one failure mode: it is never wrong in a way you can see. A name it does not know becomes "a woman"; a year becomes "the 1950s"; a place becomes "a garden". The sentence reads beautifully and quietly overwrites the truth.

Verso is built to fail the other way.

Three mechanisms, in order of how much I trust them:

  1. The facts are not generated. Who, where, when are printed from the answers, verbatim, by the app. The model only writes the label and the sentence, and the prompt is not allowed to repeat names or dates. A blank prints as not known.
  2. A guard against invented proper nouns. Inside a sentence, a capital letter means a proper noun. So the app tokenises the model's title and caption and rejects any capitalised word — or four-digit year — that the owner never typed. Names off by a letter are not licensed: "Ana" does not allow "Anabel". A caught draft is thrown away and rewritten once with the invented words named in the prompt. Caught twice, the card keeps your own words and says so in orange on its face.
  3. An ask that has to fit the hole. The app already knows which answer is blank. The model's question is dropped unless it names that subject — a who question for a blank WHO — and unless it sounds like something a person would say. Questions about "the missing information" are rejected: the back of a photo never talks about the form that produced it. If the model's ask fails, the app falls back to the plain question.

That third one is the whole product in one sentence: the app may ask, it may not answer.

The Part That Isn't Good

The captions are not literature. They are true sentences, assembled from five answers, and when the answers are thin the sentence is thin. From the calibration set, a complete card:

The group went to the step on the street on that date.

Nothing in that sentence is invented. Everything in it is flat. On the demo card the model also spelled the year out — "in nineteen fifty eight" — which is true, correct, and not how anyone writes.

The asks are honest but not always well-written. Four of the nine were the plainest possible question (When was this picture taken?). One was clumsy:

When was the event that took place?

And the guard has a real hole I want to name rather than hide: it checks capitalised words and years, not common nouns. A lowercase invention — "a bicycle" — would pass. This is a strong filter, not a proof. The facts under the sentence are still the part I trust, which is why they are printed from the answers and not from the model.

The Numbers

Twenty invented photographs — written down as five answers each, not as pictures — nine with at least one blank (eleven blanks in all — six years, three names, two places). Every entry runs through the same shipping writer the app uses. Three prompt versions, same model, same device, same corpus.

Prompt version First try Rewritten once Written from your words Asks the model wrote Mean time per card
v1 — with extra rules 13/20 2/20 5/20 7/9 7.8 s
v2 — more rules 8/20 4/20 8/20 3/9 9.8 s
v3 — the current prompt 20/20 0/20 0/20 9/9 6.1 s

The finding that mattered: adding rules made it worse. v1 and v2 piled on clauses about not inventing, about the format, about being honest. Each clause was reasonable. Together they broke the three-line contract — the model started dropping labels, adding commentary, and getting caught by the guard — so the app threw the draft away and fell back to the owner's own words. The fix was subtraction: one line per output, nothing else in the contract. The guard is the safety net, not the method.

The final run is kept in calibration.json at the repo root, with every card's raw model output. Twenty cards, 20 first try, 0 caught words, 9 of 9 gaps handled, 9 of 9 asks written by the model, mean 6,138 ms per card (min 4.4 s, median 5.5 s, max 10.7 s).

Honest caveats. Twenty invented photos, written by me, is a calibration set, not a sample of the world. The timings are an x86_64 emulator with no phone-class acceleration — I do not own an arm64 Android phone, so I will not invent one's timing. The model is 2.6 GB and takes a minute to load the first time. And the corpus is invented precisely because the real thing would be other people's family photographs.

Why Open Innovation Matters

This project would be impossible to ship honestly on a closed model.

  • The photos are of dead people. A family photo archive is the most sensitive thing on a phone, and the whole premise of the app is that someone has to type names they are not sure of. Asking a family to upload the shoebox to a captioning API is not a privacy policy problem; it is a non-starter. With open weights on the phone, the photo never moves. The missing INTERNET permission is not a promise, it is a capability the app does not have.
  • Nobody pays per photo. A utility for labelling a box of old photographs has to be free at the margin. An API call per card makes the honest business model "don't build this". Local inference costs nothing to run, forever.
  • The model is a file, not a vendor. Swapping in a better or smaller open-weight model is a file swap. No pricing page, no deprecation notice.
  • Outlives the company. A family archive is a twenty-year artefact at minimum. It should not depend on an endpoint still accepting requests in 2046. Open weights, a plain JSON store and a PDF that any printer understands are the longevity plan.

For Week 4's theme: the ghost in the machine is the person in the photograph. Día de los Muertos is not built out of atmosphere — an ofrenda is specifics. Her food, her songs, her name. A model that guesses the name is the opposite of remembrance. The app is allowed to write about the photo; it is not allowed to remember for you.

Try It

Two downloads, once:

  1. Verso v0.1.0 — signed arm64 APK (42 MB, Android 9+). Sideload it; there is no Play Store listing.
  2. The model — litert-community/gemma-4-E2B-it, gemma-4-E2B-it.litertlm, about 2.6 GB. It is gated under Google's Gemma terms, so accept the licence in your browser and download it yourself.

Open the app, tap Choose the model file, and pick the download. It is copied into app storage and loaded; the first load takes a minute. Then: pick a photo, answer what you actually know, leave the rest blank, and tap Write the back.

One tip from testing: the first card after a fresh model load is the slow one.

Code

github.com/Burry071/verso — Kotlin, Jetpack Compose, LiteRT-LM 0.18.1, no Room (cards are one JSON file), no networking. 34 unit tests cover the prompt parser, the guard, the ask checks and the shelf summary. calibration.json, calibration2.json and calibration3.json are the three runs in the table; the sheets in this post are cardA.pdf and cardB.pdf from the app.

./gradlew :app:testDebugUnitTest
./gradlew :app:assembleDebug

How I Built It

Three pieces, no more:

The writer. One prompt, built from the five answers, with the blank one named. It asks for exactly three labelled lines. Temperature 0.4 on the first attempt, 0.2 on the rewrite. A fresh conversation per call — raw prefill degenerates on Gemma 4, a lesson from my last project that I did not have to learn twice.

The parser. Labelled lines first; if the model drops the labels and writes the three lines bare, a positional read — with the shape checked, so a paragraph of prose is refused rather than half-accepted. Ask strings are normalised, and anything that starts with a noun instead of a question word is dropped.

The guard. The capital-letter rule above, plus a ban on the form's own vocabulary (answer, blank, missing, unknown) so an ask cannot leak the prompt, plus the gap check. It runs on every draft, and every catch is recorded on the card.

The print sheet is Android's PdfDocument: one 288 × 432 pt page, the photo clipped to a landscape box, the title in serif, the facts in monospace, not known in pencil grey, the ask in italic blue. It is the same information as the card, on paper, which is where a family archive actually lives.

The design is deliberately paper: warm cream, ink, one faded blue, zero rounded corners. A photo card should look like it belongs in an album, not in an app store.

What's Next

  • Answer later. Today the ask is printed and lost. It should become a field on the card you can fill in next time you call your aunt.
  • A second question when two facts are blank. Right now the app asks about the first hole and stops.
  • An arm64 phone, so the timings stop being emulator timings.

Built for the DEV × Hacktoberfest Week 4 "Ghost in the Machine" challenge. The machine writes the label. It does not get to invent the name.

來源:DEV Community · dev.to