Skip to content
xy3Public

About

5 minutes of italian every day, with recording

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

brano

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.

What it does

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.

Running it

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 below

There 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.

The free tier

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 retryDelay the 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-cookies exists because session cookies are Secure by default and browsers silently discard those over plain HTTP. Without it a http://localhost login appears to work and never sticks. Never use it in production.

Layout

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.

Accuracy

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.

Notifications

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.

About

5 minutes of italian every day, with recording

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages