A daily reading exercise in Italian, generated fresh each morning, that remembers what it has already covered and adapts to how hard you found yesterday's.
Single-user by design: anyone can read the sample, one account can use it.
Every day at an hour you choose, in your timezone, it generates a ~600-word passage — roughly five minutes of reading — on a topic it has never used before. With it come comprehension questions you reveal on tap, and a glossary of the words that would actually stop you.
Three things then feed into tomorrow:
- A rating. Too hard, just right, or too easy. Two of the same in a row moves you along the CEFR ladder; the last few also ride along in the prompt so it can calibrate within a level.
- A theme. Three angles steer what tomorrow is about. Optional — skip it and it picks at random.
- Your voice. Read the passage aloud and it keeps the recording against that day, so you can hear the difference months later.
A web push reminds you when the day's passage is ready.
go build -o brano .
./brano -set-password theo # create the account (prompts, no echo)
./brano # 127.0.0.1:8384
./brano -generate-now # generate today immediately, then exit
./brano -insecure-cookies # local HTTP only — see belowThere is no signup route. The only way an account exists is
-set-password on the machine itself. Run it again to change the password.
Config at ~/.config/brano/config.json, mode 0600, holding one secret:
{
"gemini_api_key": "...",
"base_url": "https://brano.1t.ie",
"gemini_model": "gemini-flash-latest"
}VAPID keys for push are generated on first run and stored in the database — nothing to configure. Don't regenerate them; existing subscriptions are bound to the public key and would silently stop delivering.
gemini-flash-latest fails a fair share of calls with 503 "high demand",
and the free tier caps requests per minute as well as per day. Both are normal
operating conditions rather than faults, so the app treats them as such:
- The Gemini call retries transient failures itself (up to four attempts),
preferring the
retryDelaythe API sends over any interval it would guess. A single 503 no longer fails the "Genera adesso" button. - A per-minute limit is retried; the daily allowance is not — that one
parks the scheduler until the quota resets at midnight Pacific. The two look
identical apart from the
quotaId, and treating them alike once cost ~50 pointless overnight calls.
Two things that will otherwise waste your time:
web/is embedded (//go:embed), so editing HTML, CSS, or JS needs a rebuild. Restarting alone serves the old assets, which looks exactly like a caching bug and isn't one.-insecure-cookiesexists because session cookies areSecureby default and browsers silently discard those over plain HTTP. Without it ahttp://localhostlogin appears to work and never sticks. Never use it in production.
| File | |
|---|---|
main.go |
HTTP server, routing, every /api handler |
auth.go |
Password login, sessions, per-IP rate limiting |
gen.go |
Gemini client, prompt construction, response parsing |
schedule.go |
Per-minute ticker deciding who is due, in their own timezone |
level.go |
CEFR ladder and the rules for moving along it |
topics.go |
The pool of topic angles |
push.go |
VAPID keypair and web push delivery |
store.go |
SQLite schema and every query |
sample.go |
The fixed passage logged-out visitors see |
web/ |
Embedded UI |
Go standard library plus three dependencies: modernc.org/sqlite (pure Go, no
cgo, so the binary stays static), webpush-go (VAPID and payload encryption),
and x/crypto + x/term for bcrypt and no-echo password entry. Gemini is
plain stdlib HTTP.
Non-obvious decisions are commented where they apply rather than collected
here — the timezone handling in schedule.go, the generation lock, and the
has_audio subquery in store.go all have a paragraph above them explaining
what breaks without them.
The passages are model output and nothing verifies them. The first one this app generated confidently reproduced three details of the Flannan Isles lighthouse mystery that come from a 1912 poem and a later hoax rather than the record.
That matters more here than in most places: you're reading in a language you're still learning, so you're decoding sentences rather than fact-checking them, and anything false goes in unchallenged.
The system instruction now names that failure mode — write only what is documented, never invent a quotation or log entry, say when the truth is unknown, flag famous-but-added details as later embellishments. On a re-test of the same subject it produced a correct account and a sentence identifying those exact three details as inventions.
It's a mitigation, not a guarantee. If a passage tells you something surprising and you plan to repeat it, check it. The real fix is grounding with Google Search, untested here because it's unclear whether it combines with the structured output this depends on.
Sign in → Impostazioni → Attiva le notifiche → allow.
On iPhone that only works from the Home Screen app. iOS exposes the push API solely to a web app launched from the Home Screen; in Safari itself it's absent. Share → Add to Home Screen, open brano from the icon, then enable. Android and desktop work directly.