AI Tinkerers STL

Literacy Is Going the Way of Cursive

So I built the bridge tool.

Read This For Me — point a camera at any words and hear them, four different ways, from one photo.

Expo / React NativeCloud VisionGemini 3.5 Flash LiteCloud TTS

Mark Tornga · marktornga.com

Press F for presenter mode. Arrow keys to advance.

Who It Helps

54 million adults in the U.S. read below a 6th-grade level. Many navigate daily life by asking others for help, memorizing routines, or avoiding situations entirely.

Read This For Me lets people read independently and privately — no accounts, no history, no one watching. Just point at text and listen.

Perfect for reading menus at restaurants, understanding medical paperwork, decoding official letters, and navigating signs and notices.

That is the user today, and it is reason enough to build this. The rest of this section is about why I do not think the problem stays this size.

Everything Written Is Legacy Code

Short-form video already won. An entire generation gets its instructions, its news, and its recipes without reading a paragraph.

So when a process still requires reading — the clipboard of forms they hand you at the doctor's office — that is not a neutral design choice. That is a system written against a runtime that is being deprecated.

// written English, user-facing
// status:  deprecated
// shim:    LLM translation
// removal: TBD

// we are the shim.

Everyone in this room is literate, and was raised in a literate world. That makes this sound insane.

It is going to sound less insane every year.

How a Skill Dies

1

Everyone can write cursive.

It is simply how writing works.

2

You should learn cursive because I said so.

The reason is now the authority, not the use.

3

No one can explain why we’re still teaching cursive.

It survives as ritual, then it stops.

Now run the same three stages on reading.

  1. 1. Everyone can read. — you are here
  2. 2. You should learn to read because I said so.
  3. 3. No one can explain why we're still teaching reading.

The App in Ten Seconds

📸

Point

At menus, forms, mail, medicine labels, signs.

👆

Tap

Clipboard reads every word. Brain explains it.

🔊

Listen

Natural voice, adjustable speed, nothing to read.

Read This For Me screenshot 1
Read This For Me screenshot 2
Read This For Me screenshot 3
Read This For Me screenshot 4

How It Works

Read This For Me architectureOne photo from the phone camera forks into two paths. The Read path posts to the /ocr Cloud Function, which calls Google Cloud Vision TEXT_DETECTION and returns every word with no language model involved. The Explain path posts to the /explainPipeline Cloud Function, which calls Cloud Vision and then Gemini 3.5 Flash Lite to produce a short, medium, or long summary. Both paths converge on Cloud Text-to-Speech, which returns audio to the phone.ONE PHOTOexpo-cameraREADPOST /ocrCLOUD VISIONevery word · no LLMEXPLAINPOST /explainPipelineVISION → GEMINIshort · medium · longCLOUD TTSNeural2-J → audioREAD EVERY WORDEXPLAIN IT TO ME

Two paths out of one photo. The pink path never touches a language model. The explain path runs gemini-3.5-flash-lite — a single model now. It used to run a 50/50 split against an older Flash Lite so I could compare latency in production; that older model was deprecated, and the winner is both the survivor and the faster one.

One Capture, Four Outputs — and One of Them Has No AI in It

Read every word

Cloud Vision TEXT_DETECTION → TTS. That is the whole feature.

  • No model.
  • No prompt.
  • No temperature.
  • No hallucination surface.

Explain it to me

Cloud Vision → Gemini → TTS, in three lengths.

  • Short.
  • Medium.
  • Long.
  • All from one prompt template.

An explanation costs about 470 milliseconds and a fraction of a cent.

So you stop picking one rendering on the user's behalf. You hand them four and let them choose.

And knowing which half of the product should not use the LLM is most of the design. When someone needs the exact wording of a legal notice, the trustworthy answer is the deterministic one.

The Whole Pipeline Is Seven Lines

Real world → data

functions/index.js:126-144
const request = {
    image: { content: image },
    features: [{ type: "TEXT_DETECTION" }],
};

const [result] = await client.annotateImage(request);
const fullText = detections[0].description;

Data → information

functions/index.js:210-212
const result = await model.generateContent(prompt);
const response = await result.response;
const explanation = response.text();

The first block is the entire “read every word” feature.

Six lines, one API, zero prompts.

There Is Exactly One Prompt

functions/index.js:40-52
Task: Explain the following text simply and directly for someone with low literacy.

LENGTH: ◄ the only line that ever changes ►

RULES:
- Output ONLY the explanation.
- NO introductory or concluding sentences (e.g., NO "Here is a summary", NO "Important information follows").
- NO markdown formatting (no *, #, -, etc.).
- NO conversational filler.
- Use simple, direct sentences.

TEXT to simplify:
${text}

Every line except LENGTH: is fixed. Note that the rules are almost entirely negative — five things not to do, one thing to do. Most of the work in this prompt is suppressing the model's instinct to be chatty at someone who is already struggling.

Shown dedented, with the one long rule wrapped. The real template literal is indented eight spaces per line, and that whitespace is genuinely sent to Gemini.

The Entire Difference Between Short, Medium, and Long

shortLENGTH:

Give a very brief 1-2 sentence summary. Just state what this document is and its main point.

— everything above and below this line is byte-identical —

mediumLENGTH:

Give a clear explanation in 3-5 sentences. Cover the key points someone needs to know.

— everything above and below this line is byte-identical —

longLENGTH:

Give a thorough explanation covering all important details. Be comprehensive but still use simple language.

No maxOutputTokens. No temperature. No generationConfig anywhere in the repo.

The token budget is a sentence of English.

How That Prompt Got There

1f273f5

Ask nicely

One sentence. "Explain this text simply to a layperson who cannot read well." It worked, and it rambled.

65e90ef

Add negative constraints

The RULES block appears. Structure, and five explicit prohibitions. Still one fixed length.

49aac2a

Make length a parameter

"Keep it very concise." is deleted and replaced by the LENGTH slot. This one edit turned one summary into three.

0a7d941

Deduplicate

Extracted into a shared buildExplainPrompt() so /explain and /explainPipeline cannot drift apart.

Free-form request → structured constraints → the varying part becomes a parameter. Same arc as any other refactor.

Three Answers, One Photo

The OCR text is cached on the device, so changing length re-calls /explain with the same string. No second capture, no second Vision call.

app/index.tsx:386-407
const handleLengthChange = async (newLength: ExplanationLength) => {
    if (newLength === explanationLength) return;

    setExplanationLength(newLength);

    // If we have original OCR text, re-explain with new length
    if (isExplainMode && originalOcrText) {
        await stop();
        const explanation = await explainText(originalOcrText, newLength);
        setActiveText(explanation);
        await speak(explanation);
    }
};

Output 1: Read Every Word

Illustrative example — not a live capture
PATIENT REGISTRATION & FINANCIAL RESPONSIBILITY

ASSIGNMENT OF BENEFITS
I hereby authorize payment directly to the
provider of benefits otherwise payable to me,
but not to exceed the provider's regular charges
for this period of treatment. I understand that
I am financially responsible for all charges
whether or not paid by insurance.

NOTICE: Balances not paid within 90 days of the
statement date may be referred to an outside
collection agency. A $35 fee applies to any
returned check or declined payment method.
Appointments cancelled less than 24 hours in
advance are subject to a $50 missed-visit
charge, not billable to your insurance.

Signature ______________  Date __________

Cloud Vision output, spoken back verbatim. No model touched this.

This is what you get today if you ask the receptionist to read it to you — assuming you are willing to ask a stranger to read your medical paperwork out loud in a waiting room.

Outputs 2, 3, 4: Same Photo, One Prompt

Illustrative example

short

23 words

This is a form from the doctor’s office about paying your bill. It says you have to pay whatever your insurance does not.

medium

69 words

This is a registration form from the doctor’s office. When you sign it, you let your insurance pay the doctor directly, but you still owe anything the insurance does not cover. If a bill goes unpaid for 90 days, it can be sent to a collection agency. If you cancel less than a day before your visit, you will be charged 50 dollars, and insurance will not pay that.

long

123 words

This is a form the doctor’s office needs before your visit. Fill in every part, because a missing answer can delay your appointment. When you sign it, you are telling your insurance company to send payment straight to the doctor instead of to you. You are still responsible for the whole bill. If your insurance pays part of it, or none of it, the rest is yours to pay. If a bill sits unpaid for 90 days, the office can hand it to a collection agency. If a check bounces or a card is declined, they add a 35 dollar fee. If you cancel with less than 24 hours notice, you will be charged 50 dollars, and your insurance will not cover that.

Written by hand for these slides to match each prompt's stated constraint — not captured from a live run. The point is the shape of the four outputs, not the wording.

Where the Time Actually Goes

Median warm latency, measured from 82 production log entries on 19 August 2026.

562ms
Cloud Vision (OCR)
467ms
Gemini 3.5 Flash Lite
228ms
First TTS sentence

The first call of the day cost 5,135ms

That is one cold Vision call from this morning's logs — 9x the warm median. Cloud Scheduler now pings the real endpoints every five minutes for about fifteen cents a month. The gotcha: a dedicated warm-up function is a separate Cloud Run container and warms nothing.

Start talking before you finish thinking

TTS runs one sentence at a time, and sentence N+1 loads while N plays. Only the first sentence blocks audio — and that sentence comes back in 228ms. The rest of the pipeline keeps working while the user is already listening.