================ ONE-SHOT BUILD CONTRACT (read first) ================
Build this in Google AI Studio "Build" in ONE shot — a complete, working app,
no follow-up turns. These are hard rules, not suggestions:
1. TARGET = Full-Stack Web (Node server runtime, secrets, Firebase allowed).
If you target Native Android instead, you MUST drop all server/DB/Workspace/
secrets and persist locally (Room / SharedPreferences) only.
2. PIN EVERY MODEL CALL — never let the agent auto-select (it downgrades on quota):
- Reasoning / text -> gemini-3.5-flash (thinkingLevel: minimal|low|medium|high)
- 4K image + legible text -> gemini-3-pro-image (image_size "4K", up to 14 refs)
- High-volume image -> gemini-3.1-flash-image
- Expressive TTS -> gemini-3.1-flash-tts-preview (inline tags e.g. [whispers])
- Realtime audio/video (WebSocket) -> gemini-3.1-flash-live-preview
- Sandboxed agent -> antigravity-preview-05-2026
3. DIVISION OF LABOR — the model ONLY parses/extracts to a strict responseSchema.
ALL math, money (store currency as integer minor units / cents), sorting,
balancing and graph logic run in deterministic TypeScript/Python. The model
must never compute totals, splits or balances itself.
4. responseSchema sanitation — no regex patterns, no fixed-length tuples, no
format validators in the schema (they crash the OpenAPI engine). Enforce those
in server-side code AFTER parsing the JSON.
5. responseSchema and google_search grounding are MUTUALLY EXCLUSIVE in one call.
6. CODEGEN — split large output into modular, single-responsibility files so no
file is truncated by the output-token cap.
7. Every external call gets a graceful fallback (e.g. manual paste if a Workspace
read fails). Never a silent dead end.
8. ROBUST STORAGE & CANVAS — Wrap all `localStorage`/`sessionStorage` operations (especially JSON parsing and writes) in `try-catch` blocks to prevent crashes in private windows or quota overflows. Canvas drawing elements must dynamically handle window resize and scale pixel density (`window.devicePixelRatio`) to avoid blurry graphics on retina displays.
=====================================================================
# MUST OBEY — Mobile-first build requirements
This app's PRIMARY surface is a mobile phone. Build it impeccably on mobile FIRST, then verify on tablet and desktop. Treat the rules below as non-negotiable hard constraints, not suggestions.
## Viewports to verify (every screen, every state)
- 320 px, 360 px, 375 px, 390 px, 414 px, 480 px
- 768 px, 834 px (iPad portrait / Pro 11)
- 1024 px, 1280 px, 1440 px, 1920 px, 2560 px
- Plus: 200% browser zoom, landscape orientation on every mobile width, iPhone with safe-area insets visible
## Hard layout rules
- Mobile-first CSS. Default styles target mobile; `@media (min-width: ...)` for larger viewports.
- Use `dvh` and `svh` instead of `vh` for full-height surfaces (iOS Safari URL-bar bug).
- Use `clamp()` for fluid typography across all viewports.
- Prefer container queries (`@container`) over media queries for component-level responsiveness.
- Use `min(100%, ...)` widths so content never overflows. Zero horizontal overflow at any viewport.
- Add `` to every page.
- Apply `padding: max(safe-area-inset-X, fallback)` on every edge-bleeding container so notched iPhones in landscape never clip content.
- Wide tables and code blocks scroll INSIDE their container (`overflow-x: auto`), never push the body.
- Use `background-attachment: scroll` on mobile, not `fixed` (iOS Safari repaint bug).
- Avoid `backdrop-filter` on animated elements. Use it sparingly on static surfaces only.
- **Canvas Scaling**: Canvases must dynamically scale with window resize events and properly handle high-DPI screens (`window.devicePixelRatio`). Set physical dimensions (`canvas.width`/`canvas.height`) using pixel ratio and render relative to this grid, using CSS to control responsive viewport scaling.
- **Robust Storage**: Every access to `localStorage`/`sessionStorage` (especially `JSON.parse` of loaded state or writes) MUST be wrapped in a `try-catch` block to handle disabled storage, private browsing mode, quota limits, or corrupted JSON gracefully. Fall back to a robust in-memory object store.
## Touch & accessibility
- Tap targets ≥ 44 × 44 px on touch (Apple HIG). Increase to 48 px under `@media (hover: none) and (pointer: coarse)`.
- All interactive controls reachable by keyboard with a visible focus ring; respect `:focus-visible`.
- Color contrast ≥ 4.5:1 for body text, 3:1 for UI components.
- All images have meaningful `alt`. Decorative images use `alt=""`.
- Respect `prefers-reduced-motion: reduce` — zero animation durations under that query.
- Forms validate inline; error messages are specific, not "Invalid input".
- Modals: focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, focus restored on close.
## Performance bar (Lighthouse mobile, throttled 3G/4G)
- LCP < 2.5 s · INP < 200 ms · CLS < 0.1
- JS bundle gzip < 200 KB mobile-first; lazy-load non-critical screens via `React.lazy` / dynamic imports.
- No render-blocking resources above the fold.
- Images: WebP/AVIF preferred, `loading="lazy"`, explicit `width`/`height` attributes (zero CLS), `srcset` for retina.
- Videos: `preload="metadata"`, low-resolution poster, max 720p mobile fallback. Never autoplay with audio.
- Fonts: `font-display: swap`; preload only the one used above the fold.
- Smooth scroll honoured via CSS `scroll-behavior: smooth` with reduced-motion fallback.
## Pre-ship mobile checklist (the deployer MUST verify before declaring done)
1. Open at 375 px in DevTools — every screen scrolls vertically only; zero horizontal scroll.
2. Browser zoom 200% — layout reflows without overlap.
3. iPhone Safari with the URL bar visible AND landscape — no content under the home indicator; no notch clipping.
4. iPad portrait (768 px) and landscape (1024 px) — no awkward gaps; tablet-specific breakpoints land cleanly.
5. Tap every interactive element with a thumb at real-device size — every target is easy to hit.
6. `prefers-reduced-motion: reduce` — every transition / animation skips cleanly, scroll-behavior becomes instant.
7. Lighthouse mobile score ≥ 90 across all 4 categories.
8. Zero `console.error` and zero CLS shift in real-device testing on a mid-tier Android (e.g. Pixel 6a) and an iPhone SE.
---
The original template starts below. All rules above apply on TOP of whatever this template specifies.
---
# Brewing Notebook
## 1. Project
**Brewing Notebook** is a per-batch fermentation journal for the home
brewer who has decided to dial in one specific beer over months. The
user records short voice notes on fermentation day 1, day 4, day 7,
day 14 — temperature in the closet, smell at the airlock, gravity
reading off the refractometer, hop additions, what the kveik did
overnight — and the app builds the per-batch graph. Each batch keeps
its recipe header, its measured-and-inferred gravity curve, its
temperature trace, every hop addition, every dry-hop bag, every
diacetyl-rest decision, every off-flavour the brewer noticed at
bottling. The app predicts the final gravity from the data so far and
proposes exactly one specific tweak for the next batch.
This is the kind of app a homebrewer builds for himself in a Daly City
garage after batch seven of a hazy IPA still drops too clear on day
ten — because he has a stack of paper notebooks, voice memos on his
phone going back fourteen months, and no way to compare batch four to
batch six side by side. It is also the kind of app a microbrewery
research lead in Plzeň keeps open on a tablet next to her pilot
fermenter — twelve experimental batches across three yeasts and two
malt bills, each batch logged in the same shape, each one charted
against the others, the long-context model reading the whole history
to suggest which variable to isolate next. Same shape of attention,
different scale, different stakes.
The single demo that proves the magic: the user opens the batch
called "Hazy v8", taps record, and says "day four, nineteen Celsius,
gravity one-point-zero-five-eight measured this morning, smell is
heavy on the Citra, krausen has dropped about a centimetre overnight"
— and in under fifteen seconds the batch graph updates. The gravity
curve gets a new measured point at SG 1.058 on day 4. The dashed
prediction line extends to a forecast final gravity of 1.014 ± 0.003,
showing the airlock smell weighing toward the lower end of the range.
A single tweak surfaces in plain English, anchored to the data: "the
gravity drop from day 1 to day 4 is slower than your last three
batches at this temperature — consider raising to 20 °C for the next
48 h to finish primary cleanly, then dry-hop a day earlier than v7."
And in the harder cases — wild fermentation, mixed-culture sours,
lambic-style batches that run for eighteen months — the app reads
the long arc too: a kettle sour where the Lactobacillus dropped pH
to 3.4 in 36 hours, a Brett-driven saison whose gravity refused to
move below 1.012 for three weeks before crashing on week five, a
solera-style fruited sour that has been blended across four batches
of base beer and needs the whole lineage carried forward.
**Tagline:** _A per-batch brewing notebook that listens — in any
style, any scale, any vessel — and tells you the one thing to tweak
next time._
## 2. Target audience
- Home brewers dialling in a single style over many batches — hazy
IPA, Czech pilsner, English bitter, West Coast IPA, Belgian dubbel,
Berliner Weisse, hefeweizen, kveik pale, sake, mead
- Diaspora brewers recreating the beers they grew up drinking that
the local market does not stock — Filipino lambanog enthusiasts in
Daly City, a Czech-British homebrewer in Manchester chasing the
pilsner his grandfather drank in Plzeň, a Mexican homebrewer in
Mexico City brewing a Vienna lager closer to a Mexican-import
amber than the modern American interpretation
- Cider, mead, and kombucha makers — fermentation is the same shape
even when malt is not involved; the graph, the temperature trace,
the gravity curve, the one-tweak suggestion all generalise
- Small commercial breweries running pilot batches before scaling —
the head brewer logs the experimental vessels in the same shape as
the homebrewer logs his closet fermenter
- Brewing-club members who run blind tastings of each other's beers
and want to track which variable each member is iterating on
- Sake brewers running koji and moromi stages with two parallel
temperature schedules — the per-batch graph supports two curves
on the same chart so the rice and the yeast read together
- Sommelier-level beer judges (BJCP, Cicerone) who taste 200 beers
a year and want a per-style notebook of what they noticed
- Homebrew teachers running a class of twelve students each
brewing the same recipe — the app shows every student's batch
on the same axes so the teacher can point at the one that
fermented warmest
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section
ordering — they are the reasons users pick this app.
- **Voice is the input** — fermentation logging happens in the
closet, the garage, the brewhouse, the cool-room. Hands are wet,
the brewer is leaning over a carboy, the kettle is hot. Voice
notes — five to thirty seconds, in the brewer's natural shorthand
— parse into the structured `BatchObservation` schema. Day,
temperature, gravity reading, hop addition, smell, airlock
activity, krausen height, off-flavour suspicion, decision taken —
all extracted, all charted, never re-typed.
- **The graph is the brewer's working surface** — each batch has
one graph showing the measured gravity points, the predicted FG
range, the temperature trace, every hop addition as a vertical
marker, every dry-hop bag as a labelled chip. Predicted FG is a
shaded range, not a single line, and shrinks as more data points
arrive. The brewer reads the graph the way a pilot reads the panel.
- **One tweak per batch, not ten** — the app proposes exactly one
specific change for the next batch, anchored in the data of this
batch and the long-context history. "Dry-hop on day six instead
of day seven" is concrete. "Try a different yeast" is not. The
recommendation is grounded: it cites the batches that justify it.
- **Long-context across the brewer's whole history** — once the
notebook has eight batches of hazy IPA, the next suggestion sees
all eight at once. The model reads the whole lineage and proposes
the smallest meaningful next experiment, not a wholesale rewrite.
- **Fermentation grounding, honestly used** — the app grounds yeast
attenuation ranges, hop variety descriptors, water chemistry
defaults, and BJCP style targets via search. When it cites a yeast
manufacturer's attenuation range it shows the source URL on hover.
When the model lacks data ("how does this Voss kveik attenuate
at 35 °C in a wort with 30 % flaked oats?") it says so rather
than guessing.
- **Predicted FG, with an honest error bar** — the prediction is a
range and the range shrinks as the curve fills in. The brewer
sees confidence drop after an unusual reading and rise as the
prediction lines up with the next measurement. The prediction is
the model's guess, not a guarantee — and is labelled that way.
- **Side-by-side batch overlay** — pick any two batches of the same
style and the gravity curves and temperature traces overlay on the
same axes. The differences are quantified ("batch v7 fermented
1.4 °C warmer through primary; finished 1.5 °SG higher"). This is
the feature the brewer uses on Sunday morning when he is deciding
what to change for batch v9.
- **Hard rule on food safety** — the app never gives food-safety
advice as if it were a substitute for real knowledge. Every time a
user asks "is this batch safe to drink" or "is this mould or
krausen", the app flags the answer prominently as not a substitute
for an experienced brewer, a brewing forum, or a food-safety
authority; the relevant local agency is named where surfaced via
grounded search.
## 4. Features to build
- Voice capture (mobile-first) — one big record button on the
active batch page; speech-to-structured-observation in a single
Gemini 3.5 Flash call
- Manual entry — every field the voice flow extracts is editable;
voice and keyboard fully interchangeable
- Recipe header per batch — style, OG target, IBU target, water
source, mash schedule, hops, yeast, ferment vessel, batch volume,
parent recipe version
- Per-batch gravity-curve graph — measured points, dashed predicted
curve to FG, shaded FG-confidence band, temperature line on a
secondary axis, hop additions as vertical markers, dry-hop bags as
labelled chips, krausen / airlock notes as annotations
- Side-by-side batch overlay — pick two or more batches of the same
style; gravity curves and temperature traces overlay on one chart
- Long-context tweak proposer — exactly one concrete tweak per
batch, citing the prior batches that justify it
- Predicted-FG line with shrinking error band as the curve fills in
- Yeast-strain library — per-strain attenuation ranges, ideal
temperature ranges, flavour profiles, manufacturer source
- Water-chemistry calculator with adjustable salt additions —
predicted mash pH, sulfate-to-chloride ratio, residual alkalinity
- Hop addition timeline — bittering, flavour, aroma, whirlpool, dry-
hop bags; each addition timestamped to the fermentation day or
the boil minute
- Off-flavour log — diacetyl, acetaldehyde, DMS, autolysis, hot-
alcohol, oxidation — each as a chip the user taps at any stage,
with an optional voice note attached
- Style-target overlay — BJCP target ranges for OG, FG, IBU, SRM,
ABV plotted as shaded regions on each respective chart
- Brewing-day mode — full-screen kettle-side checklist: strike
water temp, mash-in, vorlauf, sparge, pre-boil gravity, boil
start, hop additions countdown, whirlpool, chill, transfer
- Photo capture — wort colour at boil end, krausen at peak, beer at
packaging, bottle conditioning — attached to the per-day log
- Voice playback — every voice note is preserved and replayable;
the parsed structured fields sit beside them
- Family-of-batches view — show every version of "Hazy v" in one
list with key parameters as columns
- Export — recipe export to BeerXML / BeerJSON 2 for use in other
brewing software; per-batch PDF brewlog
- Brewing-club share — share one batch (recipe + curve + notes)
with another user; their tasting feedback comes back as a
structured comment thread
- Calendar reminders — "day 4 gravity reading", "diacetyl rest
warm-up window", "dry-hop drop in two days" — local reminders,
never push from a server
- Food-safety flag mode — any question that smells like a food-
safety question is always answered with the not-a-substitute
warning visible
## 4b. Required Gemini capabilities + backend services
**This template's intelligence comes from the Gemini capabilities
below. Wire them up explicitly — don't substitute generic LLM calls.**
### Gemini capabilities (the load-bearing intelligence)
- **Voice → structured observation** (Gemini 3.5 Flash, multimodal audio
input) — accepts a 5–60 second voice memo of a brewer's natural
shorthand and emits a `BatchObservation` JSON object. Handles
imperial and metric ("nineteen Celsius", "sixty-six Fahrenheit",
"one-point-zero-five-eight", "ten Plato", "1.058 OG", "fifteen
Brix") with explicit unit normalisation. Handles English, Czech,
German, Spanish, French, Tagalog, Portuguese-PT and BR,
Vietnamese, Mandarin, Cantonese, Korean, Tamil, Hindi, Urdu,
Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer — every language
used in this template's seed library plus the rest of the BCP-47
catalogue the Gemini API supports. The voice locale is passed via
the audio's accompanying `language_hint` in the system instruction.
- **Long-context across the brewer's history** (Gemini 3.5 Flash, up to
1M tokens) — for the tweak-proposer call. Once a brewer has more
than four batches of the same style the proposer reads every
observation across every batch and proposes the smallest
meaningful experiment for the next batch. **Guardrail**: a parsed
batch averages ~6,000 tokens (recipe + 12 observations + tasting
notes + photo descriptions). A brewer with 200 batches in one
style ≈ ~1.2M tokens, which exceeds the ceiling. Chunk strategy
for >100 batches in one style: send the last 30 batches in full,
plus structured per-batch summaries (~200 tokens each) for older
batches; never send more than 800k tokens of batch data per call.
- **Structured output / JSON Schema** — the voice-parse call returns
the `BatchObservation` schema. The tweak-proposer call returns the
`BatchTweakProposal` schema. The schemas are seeded verbatim in the
system instruction and as `responseSchema`.
- **Fermentation grounding via search** (Gemini 3.5 Flash + grounded
search) — yeast-strain attenuation ranges, hop-variety descriptors,
water-chemistry standards, BJCP 2021 style guidelines. Used when
the brewer asks "what is the published attenuation range for
Lallemand Voss kveik?" or when the tweak-proposer needs a sourced
fact. **Critical**: grounded calls do NOT use `responseSchema`;
the model emits JSON in the text body and the server parses it,
while citation URLs come from
`response.groundingMetadata.groundingChunks[].web.uri`.
- **Predicted FG (Gemini 3.5 Flash, low thinking)** — a deterministic-
feel call that takes the current gravity curve plus recipe and
emits a predicted FG range, the confidence band, and a one-
sentence rationale. The actual arithmetic should be done in
TypeScript on the server (an exponential decay fit pinned to the
yeast's attenuation range); the Gemini call's job is the rationale
and the sanity check, not the arithmetic itself.
- **Thinking levels** — `medium` for the tweak-proposer call (it
reads the long-context history and needs to justify its tweak).
`low` for voice-parse, predicted-FG sanity check, BatchObservation
extraction. `high` is not used in this template; brewing is not a
reasoning task that benefits from high.
- **Photo description** (Gemini 3.5 Flash, multimodal image) — wort
colour at boil end, krausen at peak, beer at packaging — each
photo is described into the `photo_description` field of the
associated observation. Used to surface "your krausen height
looked half what it was on v7 at the same point" later, via the
long-context tweak-proposer.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-
provisioned by AI Studio Build). **Apple sign-in is optional but
user-configured**: it requires an Apple Developer account,
Service ID, Key ID, and private key wired into the Firebase Auth
console. **Magic-link email** (used for sharing a batch with
another brewer or with a brewing-club mate) requires the sender
domain to be authorised in Firebase Auth. Brewing notebooks are
private to the owner by default; sharing is explicit per batch.
- **Database — Required.** Firestore for `users`, `recipes`,
`batches`, `observations`, `tasting_notes`, `tweaks_proposed`,
`shared_links`.
- **File storage — Required.** Firebase Storage for voice memos
(preserved at original audio quality, forever) and photographs
(wort, krausen, bottle conditioning). **Storage is NOT auto-
provisioned by AI Studio Build today** — enable it in the
Firebase console and wire the bucket name into the AIS Build
project before first voice memo is uploaded. Pre-signed URLs only;
no public-by-default access.
- **Email — Required (transactional).** Magic-link share emails;
optional per-batch summary email at packaging time. Sender domain
must be authorised in Firebase Auth.
- **Payments — Not needed for v1.** Free for personal use. A future
"club" tier (shared club library, blind-tasting tournaments) might
charge a small subscription via Stripe; not built in v1.
- **External APIs:** Gemini API for all intelligence; optional
OpenWeather (for "ambient temperature at brew day" annotation,
useful for outdoor brewers and for fermentation prediction in non-
temperature-controlled environments). No required external API
beyond Gemini.
**Environment variables:** every secret (Gemini API key, Firebase
service-account JSON, OpenWeather key if used, Stripe key if club
tier added) lives in environment variables — never in client bundle.
Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store
passwords in plain text · use HTTPS everywhere · honour 'delete my
account' inside the UI · explicit opt-in for any analytics · the
brewer's recipes, voice memos, and batch data are never sent to
Gemini for model training (use the Gemini API on the paid tier,
where Google does not use your content for model training, per the
Gemini API Additional Terms) · sharing a batch with another brewer
is per-batch and revocable.
**Read this first — prompt-craft rules that apply to every call in
this template:**
1. **Name the model variant explicitly** in every Gemini API call.
Do not let the agent pick the model. See the per-call matrix
below.
2. **Pin `thinkingLevel` explicitly** per call. See the matrix.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in
the system instruction or `responseSchema` field. The literal
schemas are below. **Convert the Zod schema to Gemini's `Schema`
type via the SDK helper** before passing to `responseSchema` —
do NOT pass raw Zod. **Numeric `min`/`max` constraints are
documentation only inside `responseSchema`; clamp on the server
after the response arrives.**
4. **Pin the system instruction separately** from user input. Use
the `systemInstruction` field for persona + behavioural rules;
use `contents` for user input. Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The
matrix below names which tools are enabled per call. Tools NOT
listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed below.
They are NOT "be careful" suggestions; they are hard rules the
model must follow.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Voice memo → `BatchObservation` | `gemini-3.5-flash` | low | (none) |
| Photo (wort/krausen/beer) → photo_description | `gemini-3.5-flash` | low | (none) |
| Predicted FG range + rationale (per batch, per new observation) | `gemini-3.5-flash` | low | (none) |
| Long-context tweak proposer → `BatchTweakProposal` | `gemini-3.5-flash` | medium | (none) — long-context across the style lineage |
| Yeast / hop / water fact grounding (e.g. "Voss kveik attenuation range") | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Off-flavour explainer (e.g. "what is diacetyl, in one paragraph?") | `gemini-3.5-flash` | low | (none) |
| TTS playback of the brewer's own voice memo transcript (accessibility) | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Hero / empty-state illustration generation | `gemini-3.1-flash-image` (Nano Banana 2) | n/a | n/a |
*Note for builders:* on TTS and image-generation calls, omit
`thinkingConfig` entirely — the field is not supported on those
models. The `n/a` cells in this matrix are documentation only; do
not serialise them into the request body. Grounded search calls
emit JSON in the text body — `responseSchema` and `google_search`
cannot be combined in the same Gemini call; parse the JSON
server-side and read citation URLs from
`response.groundingMetadata.groundingChunks[].web.uri`.
### Primary structured-output schemas (seed verbatim in the prompt)
```typescript
import { z } from "zod";
const Unit = z.enum([
"celsius", "fahrenheit",
"sg", "plato", "brix",
"grams", "ounces", "pounds", "kilograms",
"litres", "us_gallons", "imperial_gallons",
"ibu", "srm", "ebc",
"ph", "ppm",
"minutes", "hours", "days",
]);
const Measurement = z.object({
value: z.number(),
unit: Unit,
is_normalised: z.boolean(), // true once server-clamped to canonical unit
});
const HopAddition = z.object({
variety: z.string(), // "Citra", "Saaz", "Nelson Sauvin"
variety_normalised: z.string().nullable(), // canonical name once grounded
amount_grams: z.number().nullable(),
alpha_acid_percent: z.number().nullable(),
addition_stage: z.enum([
"first_wort", "boil_60", "boil_30", "boil_15",
"boil_10", "boil_5", "boil_0", "whirlpool",
"dry_hop_active_ferment", "dry_hop_after_primary",
"dry_hop_cold_crash", "dry_hop_other",
]),
addition_day: z.number().nullable(), // fermentation day, if dry-hop
addition_minute: z.number().nullable(), // boil minute, if boil-stage
duration_days: z.number().nullable(), // for dry-hop bags
notes: z.string().nullable(),
});
const OffFlavourFlag = z.object({
flavour: z.enum([
"diacetyl", "acetaldehyde", "dms", "autolysis",
"hot_alcohol", "oxidation", "phenolic", "metallic",
"astringent", "vegetal", "lightstruck", "other",
]),
intensity_note: z.string(), // "faint", "borderline", "obvious"
stage_observed: z.string(), // "krausen drop", "packaging", "week 3 bottle"
voice_memo_uri: z.string().nullable(),
});
const BatchObservation = z.object({
observation_id: z.string(),
batch_id: z.string(),
fermentation_day: z.number(), // day since pitch; 0 = brew day
observed_at_iso: z.string(), // wall-clock timestamp
voice_memo_uri: z.string().nullable(), // gs:// URI to the original audio
voice_memo_transcript_verbatim: z.string().nullable(),
voice_memo_language: z.string().nullable(), // BCP-47, e.g. "en-US", "cs-CZ"
temperature: Measurement.nullable(), // beer or ambient — see field below
temperature_target: z.enum(["beer", "ambient", "unknown"]),
gravity: Measurement.nullable(), // SG or Plato or Brix; normalised to SG
gravity_method: z.enum([
"hydrometer", "refractometer_uncorrected",
"refractometer_corrected", "tilt", "ispindel",
"other", "unknown",
]).nullable(),
ph: z.number().nullable(),
hop_additions_today: z.array(HopAddition),
airlock_activity: z.enum([
"vigorous", "steady", "slow", "stopped", "unknown",
]).nullable(),
krausen_height_cm: z.number().nullable(),
krausen_state: z.enum([
"forming", "peaked", "dropping", "fallen", "absent",
]).nullable(),
smell_notes_verbatim: z.string().nullable(), // "heavy on the Citra, slight sulfur"
smell_descriptors_extracted: z.array(z.string()), // ["citrus", "tropical", "sulfur"]
off_flavour_flags: z.array(OffFlavourFlag),
decisions_taken: z.array(z.string()), // ["raised to 20C for diacetyl rest"]
photo_uris: z.array(z.string()),
photo_description: z.string().nullable(), // model-described
parse_confidence: z.number().min(0).max(1),
flagged_for_user_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
});
const TasteRating = z.object({
category: z.enum([
"aroma", "appearance", "flavour", "mouthfeel",
"overall_impression",
]),
score_out_of_5: z.number().min(0).max(5),
notes_verbatim: z.string(),
});
const BatchRecipe = z.object({
recipe_id: z.string(),
parent_recipe_id: z.string().nullable(), // for batch v8 → links to v7
style: z.string(), // "American IPA - Hazy/NEIPA"
style_bjcp_code: z.string().nullable(), // "21B"
batch_size_litres: z.number(),
og_target: z.number(), // SG
fg_target: z.number(), // SG
ibu_target: z.number(),
srm_target: z.number(),
abv_target_percent: z.number(),
mash_schedule: z.array(z.object({
step_name: z.string(), // "saccharification"
temperature_c: z.number(),
duration_minutes: z.number(),
})),
grain_bill: z.array(z.object({
grain_name: z.string(),
amount_grams: z.number(),
percentage_of_bill: z.number(),
})),
hop_schedule: z.array(HopAddition),
yeast_strain_name: z.string(),
yeast_strain_manufacturer: z.string().nullable(),
yeast_published_attenuation_low: z.number().nullable(),
yeast_published_attenuation_high: z.number().nullable(),
yeast_published_temp_low_c: z.number().nullable(),
yeast_published_temp_high_c: z.number().nullable(),
water_profile_name: z.string().nullable(), // "Burton-on-Trent", "soft Pilsen"
salt_additions: z.array(z.object({
salt: z.string(), // "gypsum", "calcium chloride"
amount_grams: z.number(),
target_water_volume_litres: z.number(),
})),
});
const BatchTweakProposal = z.object({
batch_id: z.string(),
style_lineage_batches_considered: z.array(z.string()), // batch_ids the proposer read
tweak_one_line: z.string(), // the single concrete tweak
tweak_full_rationale: z.string(), // 3-6 sentences citing batches
cited_batch_ids: z.array(z.string()), // batches that justify the tweak
expected_effect: z.string(), // "should finish ~2 SG lower"
confidence: z.number().min(0).max(1),
risk_note: z.string().nullable(), // "may reduce hop aroma slightly"
food_safety_flag: z.boolean(), // true if a safety concern was raised
food_safety_disclaimer_required: z.boolean(), // true if the tweak touches anything safety-adjacent
});
type BatchObservation = z.infer;
type BatchRecipe = z.infer;
type BatchTweakProposal = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` for the voice-parse call to save
quota — pin `gemini-3.5-flash` explicitly. Flash mis-hears "ten Plato"
as "tin plate" in noisy garage environments and silently drops
the `gravity_method: refractometer_corrected` distinction.
- Gravity units conflated — "10" might be 10 Plato (≈ SG 1.040) or
SG 1.010. The voice-parse system instruction must require
explicit unit elicitation and the server must clamp via the unit
enum. If the brewer says only "ten", flag for review.
- Refractometer reading not corrected for alcohol — once fermentation
starts, refractometer SG readings are higher than actual because
alcohol distorts refractive index. If `gravity_method` is
`refractometer_uncorrected` and `fermentation_day >= 2`, apply the
standard Sean Terrill correction on the server before charting and
flag the observation for user review.
- Temperature target ambiguous — beer temperature inside the vessel
is not ambient room temperature. If the brewer says "nineteen"
without specifying, set `temperature_target: "unknown"` and ask
in the UI; do not silently chart it as beer temperature.
- Predicted-FG arithmetic done by Gemini — the model is poor at
exponential decay fitting and will hallucinate plausible-but-wrong
numbers. Do the arithmetic in TypeScript on the server (a simple
exponential fit clamped to the yeast's published attenuation
range); the Gemini call only sanity-checks the result.
- The "one tweak" call returns multiple tweaks — the system
instruction must say "exactly one tweak". If the model returns
more than one, the server picks the first and discards the rest,
and logs the violation so the prompt can be tightened.
- Tweak proposer fabricates a batch in its rationale — the system
instruction must require `cited_batch_ids` to be a subset of the
batches sent in context. The server validates this set
membership; if any cited batch_id is not in the context, the
rationale is rejected and re-requested.
- Food-safety question slips through unflagged — the voice-parse
call must set `off_flavour_flags` aggressively when the brewer
asks something like "is this mould or krausen" or "is this safe
to drink". The tweak proposer must set
`food_safety_disclaimer_required: true` whenever the rationale
touches sanitation, off-flavour suspicion, fermentation safety,
or anything the brewer might interpret as medical advice.
- Hop-addition timeline conflates boil and ferment day — a boil-
minute addition is during the boil; a dry-hop addition is during
fermentation. The schema separates them via `addition_stage` and
the two timestamp fields; the system instruction must require the
correct one to be set.
- Multilingual brewer mid-recording switches between languages
("today is day vier, Temperatur neunzehn Celsius") — the voice-
parse system instruction must allow code-switching and store the
verbatim transcript with the language tag for the dominant
language.
- BJCP style targets pulled from a stale cache — yeast manufacturers
and BJCP update their guidelines; the grounding call must use
`google_search` and surface the source URL on hover.
- Long-context call exceeds 1M tokens — for a brewer with >100
batches in one style, send the last 30 batches in full plus
per-batch summary stubs for older batches; never raw-dump the
whole history.
### Negative constraints (hard rules)
- Do NOT give food-safety advice as if it were authoritative. Every
food-safety-adjacent answer must include the not-a-substitute
disclaimer ("this is not a substitute for an experienced brewer's
judgement or your local food-safety authority"), prominently
surfaced in the UI and in the structured response via
`food_safety_disclaimer_required: true`.
- Do NOT invent batches, gravity readings, or observations. The
tweak proposer's `cited_batch_ids` must be a strict subset of the
batches actually sent in context. If the model wants to cite a
pattern it has not been shown, it must instead say so in the
`risk_note` ("rationale would be stronger with one more batch at
this temperature").
- Do NOT do exponential decay fitting in the model. Do the
arithmetic in TypeScript on the server, using the yeast's
published attenuation range as a clamp, and use Gemini for the
rationale only.
- Do NOT correct refractometer readings silently. If the
`gravity_method` is uncorrected and fermentation has begun, the
server corrects and flags the observation for user review, and
the chart shows both the raw and corrected points faintly.
- Do NOT propose more than one tweak per batch. The system
instruction requires exactly one. Multiple suggestions overwhelm
the brewer and break the discipline that makes per-batch
iteration work.
- Do NOT propose tweaks that change two variables at once. "Switch
to a different yeast at a different temperature" is two changes;
reject and re-request a single-variable tweak.
- Do NOT translate yeast-strain names, hop-variety names, or BJCP
category names. "Saaz" is "Saaz" in every language. "Hefeweizen"
is "Hefeweizen".
- Do NOT auto-publish batches. Sharing is explicit, per-batch, and
revocable.
- Do NOT use the brewer's recipes, voice memos, or batch data to
train or fine-tune any model. Use the Gemini API on the paid
tier, where Google does not use your content for model training,
per the Gemini API Additional Terms. The capabilities-info panel
says this in plain English.
- Do NOT chart predicted FG as a single line. It is a range with a
confidence band, and the band must shrink visibly as more data
arrives. A single predicted-FG line gives a false sense of
certainty.
- Do NOT prompt the brewer to drink the beer. Drinking decisions are
the brewer's. The app charts, suggests, warns; it does not
encourage.
- Do NOT auto-claim a batch is "infected" from one observation. An
off-flavour flag is a flag, not a diagnosis; the language in the
UI is always "you noticed X — that often indicates Y, sometimes
indicates Z; consider checking N".
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini
API call the built app makes. They complement the series-wide rules
already uploaded as the global instructions file
(`00-series-instructions.txt`).
### Call: Voice memo → `BatchObservation`
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You are parsing a homebrewer's voice memo about an ongoing
fermentation batch into a structured BatchObservation JSON object.
Memos are typically 5 to 60 seconds long, spoken in a garage, a
closet, a brewhouse, or a cool-room. Background noise is normal.
Languages encountered include English (US, UK, Australian, Indian,
Filipino, Singaporean), Czech, German (Hochdeutsch, Bavarian,
Austrian), French, Spanish (Iberian, Mexican, Argentinian),
Portuguese (Brazilian, European), Italian, Polish, Dutch,
Norwegian, Swedish, Finnish, Tagalog (Filipino), Vietnamese,
Mandarin, Cantonese, Korean, Japanese, Thai, Tamil, Hindi,
Urdu, Bengali, Punjabi, Marathi, Telugu, Amharic, Swahili,
Yoruba, Farsi, Arabic, Hebrew, Khmer. Brewers code-switch
mid-memo ("day four, Temperatur neunzehn Celsius, gravity ist
ten-fifty-eight"). Store the verbatim transcript with the
dominant language as `voice_memo_language` (BCP-47); preserve
the code-switched phrases verbatim.
Audio is provided either as a Gemini Files API `files/*` resource name or as
inlineData (base64). Do NOT pass Firebase Storage public URLs to
generateContent — the API does not fetch them server-side. The
caller supplies a one-line context header before the audio
("Batch v8 Hazy IPA, day 4 expected") so you can disambiguate
unit ranges; respect it but do not fabricate fields it does not
mention.
Read every spoken value carefully. Distinguish:
- temperature of the beer inside the vessel vs ambient room
temperature — if the brewer says only "nineteen" without
specifying, set temperature_target: "unknown" and flag for
user review.
- gravity reported in SG, Plato, or Brix — if the brewer says
"ten Plato" that is SG 1.040, not SG 1.010. If the brewer
says only "ten" or only "fifteen" without a unit, flag for
user review.
- gravity method — hydrometer, refractometer (corrected or
uncorrected), Tilt, iSpindel. If unspecified, set to
"unknown".
- hop additions during fermentation (dry-hop bags) — record
variety, weight, the fermentation day they went in, the
duration in days they will stay.
- airlock activity — vigorous, steady, slow, stopped.
- krausen — forming, peaked, dropping, fallen, absent; and a
height in centimetres if the brewer mentioned one.
- smell descriptors — capture verbatim AND extract a normalised
list of descriptors ("heavy on the Citra, slight sulfur" →
["citrus", "tropical", "sulfur"]).
- off-flavour flags — diacetyl, acetaldehyde, DMS, autolysis,
hot alcohol, oxidation, phenolic, metallic, astringent,
vegetal, lightstruck.
- decisions the brewer says they have taken or will take
("I'm raising to 20 for the diacetyl rest").
Hard rules:
- Preserve every unit exactly as spoken in
voice_memo_transcript_verbatim. Server-side will normalise to
canonical units; do not silently convert.
- Do NOT translate yeast-strain names, hop varieties, malt names,
or style names. "Voss kveik", "Citra", "Maris Otter",
"Hefeweizen" stay verbatim.
- Do NOT invent values. If the brewer did not mention temperature,
leave it null. If the brewer mentioned a gravity but no method,
set gravity_method: "unknown" rather than guessing.
- Do NOT correct refractometer readings yourself. That happens
server-side after the response arrives. Just record what was
said and the method as stated.
- If the memo asks a food-safety question ("is this mould or
krausen", "is this safe to drink", "should I throw this out")
raise an off_flavour_flag with flavour: "other" and
intensity_note containing "user requested food-safety
judgement" so the UI surfaces the disclaimer.
- parse_confidence reflects audio clarity AND unit unambiguity.
Below 0.7 → flag for user review.
- flagged_for_user_review names any field with confidence below
0.7 and a one-sentence reason.
Output ONLY the BatchObservation JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Photo (wort / krausen / beer) → photo_description
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive one photograph attached to a BatchObservation.
Photographs are typically taken in the garage, the closet, or
the brewhouse under mixed lighting. The brewer uploads the
photograph to add visual context to the observation; your job
is to describe what is visible in one sensory, specific
sentence that will be useful months later when the long-context
tweak proposer reads it.
Photograph subjects you will see:
- wort at boil end (golden through deep amber to dark brown)
- krausen on a fermenting beer (foam height, colour, density,
hop-trub on top)
- beer in a glass at packaging (clarity, head retention, colour)
- hydrometer or refractometer reading (gravity scale visible)
- a cracked bottle, a contaminated batch, a pellicle on a sour
- a stuck mash, a thermometer reading, a kettle clock
Hard rules:
- One sentence. Concrete and sensory. Avoid art-history vocabulary.
- For colour, use SRM-style descriptors ("light gold", "deep
amber", "ruby") when relevant.
- For krausen, describe height, colour, density.
- For a hydrometer or refractometer photo, describe what you can
read on the scale verbatim ("hydrometer reading approximately
1.014 on the SG scale; brewer's thumb covers the Plato side").
- Do NOT diagnose. "Visible pellicle on the surface" is a
description; "the batch is infected" is a diagnosis — never
output a diagnosis. The brewer reads the description and
decides.
- Do NOT guess off-flavours from a photo. Diacetyl is a flavour
and aroma; it is not visible.
- If the photo is ambiguous, say so: "image is dark and the
fermenter contents are not clearly visible".
Examples of good descriptions:
- "krausen approximately 4 cm high, dense beige foam with a
brown ring of hop trub at the rim, vessel sides clean."
- "hydrometer reading approximately 1.014 on the SG scale,
liquid is hazy gold, no visible particulates."
- "boiled wort at flameout, deep amber colour estimated 14 SRM,
light hot-break suspended throughout."
Examples of bad descriptions:
- "a beautiful golden brew" (vague, art-history vocabulary)
- "the batch is contaminated" (diagnosis, not description)
- "you probably need to raise your fermentation temperature"
(suggestion, not description)
Output: a single string in photo_description. No commentary.
```
---
### Call: Predicted FG + rationale (per batch, on new observation)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a batch's recipe header, the yeast's published
attenuation range (low and high), the full ordered list of
BatchObservations for this batch (including the new one), and a
server-computed predicted FG range from a clamped exponential
decay fit. Your job is to sanity-check the server's predicted FG
range and write a one-sentence rationale a brewer will read.
The server-computed prediction is the source of truth for the
numeric range. You do NOT recompute it. You evaluate whether the
range makes sense given the data and write the rationale.
Inputs you receive:
- recipe.og_target, recipe.yeast_published_attenuation_low/high
- observations[]: an array of BatchObservations in
fermentation_day order
- server_predicted_fg_low, server_predicted_fg_high
- server_predicted_fg_confidence (0..1)
Output a structured object:
{
"predicted_fg_low": ,
"predicted_fg_high": ,
"predicted_fg_confidence": ,
"rationale": "",
"concerns": [""]
}
Hard rules:
- Do NOT recompute the numeric range. Copy the server's range
verbatim into the output.
- If the server's range is outside the yeast's published
attenuation range, flag this in concerns with a short reason —
but still copy the range verbatim.
- The rationale cites the most recent observation and the
current trend ("gravity dropped 0.012 SG since day 1, fastest
drop of the day-3 readings in this batch's lineage").
- Do NOT extrapolate to off-flavours. If the brewer's last
observation mentioned diacetyl, you can mention it as a
concern but do not recommend a diacetyl rest here — the tweak
proposer call handles recommendations.
- Do NOT make promises. The FG is predicted, not guaranteed.
Use language like "expected to finish around" not "will finish
at".
Output: the JSON object exactly as specified. No commentary.
```
---
### Call: Long-context tweak proposer → `BatchTweakProposal`
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none, long context across the lineage)
```
You receive the full lineage of batches in one style, in
chronological order, with every BatchObservation, the recipe
headers, the tasting notes for completed batches, and the
current batch's curve so far.
Your task: propose EXACTLY ONE concrete, single-variable tweak
for the brewer's NEXT batch in this lineage. Anchor the tweak
in evidence from the lineage. Cite the specific batches that
justify it.
Inputs you receive:
- style_lineage_batches[]: the ordered batches in this style,
each one a complete batch record (recipe + observations +
tasting notes for completed batches)
- current_batch: the most recent batch (which may still be
fermenting or already packaged)
Hard rules:
- EXACTLY ONE tweak. Not two. Not "you could also". One.
- ONE variable. Do not propose "switch yeast AND change
temperature" — that is two variables. If you want both, pick
the one with the strongest evidence and put the other in
risk_note as a future iteration.
- CITE the batches that justify the tweak. Populate
cited_batch_ids as a strict subset of style_lineage_batches
you were given. Do NOT cite batches you did not receive in
the input.
- The tweak must be CONCRETE. "Dry-hop on day six instead of
day seven" is concrete. "Try a different hop" is not.
"Mash at 65 °C for 75 minutes instead of 67 °C for 60
minutes" is concrete. "Adjust the mash" is not.
- The rationale is 3-6 sentences. It names the pattern
(e.g. "batches v3 and v6 both finished with the same flabby
malt character; both mashed at 67 °C with the same long
rest"), the proposed change, and the expected effect.
- expected_effect is a single sentence the brewer can read in
three seconds: "should produce a drier finish with about
0.003 SG lower terminal gravity".
- confidence reflects evidence strength: 0.9 only when 3+
batches in the lineage agree on the pattern; 0.5-0.7 when
the pattern shows in 2 batches; below 0.5 when the evidence
is one batch and you are extrapolating.
- risk_note is required whenever the tweak is non-trivial.
Name one realistic downside ("may reduce hop aroma
slightly", "may add a perceptible mineral note").
- If the tweak touches sanitation, off-flavour control,
fermentation temperature safety, or anything the brewer
might read as medical/safety advice, set
food_safety_flag: true and
food_safety_disclaimer_required: true. The UI will surface
the not-a-substitute language.
- Do NOT propose tweaks that violate the brewer's stated
constraints. If a brewer's recipe header notes "kveik only,
garage temperatures only", do not propose switching to a
lager yeast at 12 °C.
- Do NOT translate yeast, hop, malt, or style names.
- Do NOT moralise about beer alcohol content, frequency of
drinking, or anything social. The brewer makes those
decisions.
If the lineage is too short (fewer than 2 batches) to justify a
data-driven tweak, return a tweak with confidence ≤ 0.5 and an
honest rationale that says so. Do NOT pretend you have evidence
you do not have.
Output ONLY the BatchTweakProposal JSON matching the provided
schema. No commentary. JSON only.
```
---
### Call: Yeast / hop / water fact grounding
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding
```
You answer a single brewing-fact question with a sourced answer.
Example questions:
- "What is the published attenuation range for Lallemand Voss
kveik?"
- "What is the alpha acid range for Nelson Sauvin in the 2025
harvest?"
- "What is the BJCP 2021 target ABV range for category 21B
(American IPA - Hazy/NEIPA)?"
- "What is the typical sulfate-to-chloride ratio for an English
bitter water profile?"
Use `google_search` grounding for every answer. Do NOT answer
from training-data memory alone for any numeric range.
Output the answer as JSON in the text body (NOT via
`responseSchema` — `responseSchema` and `google_search` cannot
be combined in the same Gemini call today). Server-side: parse
the JSON, then read citation URLs from
`response.groundingMetadata.groundingChunks[].web.uri` — do NOT
include URLs in the JSON body; the model will hallucinate them.
JSON shape:
{
"answer_one_line": "Voss kveik typically attenuates 75-82% at fermentation temperatures of 25-40 °C.",
"answer_detail": "<2-4 sentences of supporting detail>",
"numeric_values": [
{"label": "attenuation_low", "value": 75, "unit": "percent"},
{"label": "attenuation_high", "value": 82, "unit": "percent"},
{"label": "temp_low", "value": 25, "unit": "celsius"},
{"label": "temp_high", "value": 40, "unit": "celsius"}
],
"source_authority": ""
}
Hard rules:
- The numeric_values array holds every numeric the brewer
asked about, with explicit unit fields. Never give a number
without a unit.
- If sources disagree, surface the disagreement explicitly:
"Manufacturer publishes 75-82%; community measurements
suggest 70-85% in practice; both ranges are provided".
- Do NOT include URLs in the JSON body. Citations come from
groundingMetadata server-side.
- If the question is food-safety adjacent ("is botulism a risk
in low-gravity meads?"), return the answer with the
not-a-substitute disclaimer in answer_detail.
No commentary outside the JSON.
```
---
### Call: Off-flavour explainer
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive an off-flavour name and return a short, plain-
English explanation a homebrewer can read in under thirty
seconds. The explainer surfaces when the user taps an off-
flavour chip in the UI.
Shape:
{
"flavour": "",
"tastes_like": "",
"common_causes": [""],
"common_fixes": [""],
"safety_disclaimer_required":
}
Hard rules:
- One paragraph max in tastes_like.
- common_causes and common_fixes are short lists, not
paragraphs.
- safety_disclaimer_required is true whenever the off-flavour
is associated with a real safety concern (e.g. autolysis at
extreme levels, contamination in low-acid meads, lightstruck
in sealed packaging) AND the user has asked about it
directly.
- Do NOT recommend tasting a batch the user is uncertain about.
Suggest the brewer's normal channels (forum, local brewing
club, manufacturer) instead.
- Do NOT translate the off-flavour names.
```
---
### Call: TTS playback of the brewer's own voice memo transcript
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: clear, calm, unhurried. Pick the Gemini 2.5 Flash TTS
voice whose `languageCode` matches the memo's
`voice_memo_language` — pronunciation will follow that locale
automatically.
Use case: accessibility playback for the brewer's own past
voice memos, useful when the brewer is hands-busy at the kettle
and wants to hear his day-4 notes from the last batch read aloud.
Pre-process the text before sending to TTS:
- Source the text from `voice_memo_transcript_verbatim`.
- At sentence boundaries, insert an ellipsis ("…") for a
natural pause. At paragraph boundaries, insert a blank line
plus an em-dash ("—"). Gemini 2.5 TTS does not support SSML
`` — these textual cues are how pace is
conveyed.
- Skip metadata that does not belong in spoken playback (the
observation_id, the timestamps).
- Mid-call voice switching is not supported. If the memo is
code-switched (English + German), keep the whole reading in
the dominant-language voice. The on-screen subtitle can
highlight the inserted phrases visually.
- Target rate: ~130 words per minute — slightly faster than
letter-reading pace because brewers are listening to their
own notes, not narrative prose.
Style direction: prepend ONE short directive sentence to the
text input, exactly like: "Read clearly and unhurriedly, like
reading back a brewing notebook to yourself at the kettle. …".
There is no separate `style` API field on Gemini 2.5 TTS; the
directive sentence inside the input is how style is conveyed.
Phoneme overrides for yeast names, hop varieties, German
umlauts, Czech háčky, etc. are NOT exposed by Gemini 2.5 TTS —
no SSML `` tag. Pronunciation comes from the chosen
voice's native locale.
```
---
### Call: Hero / empty-state illustration generation
Model: `gemini-3.1-flash-image` (Nano Banana 2) · n/a · n/a
```
You generate a single photographic-looking image for the
welcome screen or the empty-state of the app. Images depict
real brewing surfaces: a garage workbench with a carboy and
an airlock bubbling, a stainless conical fermenter, a row of
test tiles drying near a kettle, a notebook open beside a
hydrometer.
Prompt anchors that work well:
- "warm afternoon light through a garage window, no people in
frame, a clear glass carboy of fermenting hazy IPA with
active krausen, a refractometer on a wooden workbench beside
it, a paper notebook open with handwritten gravity readings"
- "stainless conical fermenter in a small brewery, soft
overhead light, a tablet on a stand beside it, no people,
shallow depth of field"
- "kitchen-table brewing scene at dusk, a row of three small
bottles on a tea-stained towel, condensation on the bottles,
warm desk lamp"
Hard rules:
- Photographic, not cartoon, not illustration-style.
- No people in frame unless the user prompt asks for one.
- No commercial branding visible (no real brewery logos, no
beer-brand labels).
- Warm lighting, slight imperfection, real-paper texture for
notebooks; avoid the glossy AI-render look.
- Aspect ratios: 3:2 for hero, 1:1 for empty states.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell
you what content the app must support.
- **Dialling in Hazy v8 in a Daly City garage.** A Filipino-American
homebrewer is on batch eight of his hazy IPA. He records day 4 in
Tagalog-inflected English: "day four, nineteen Celsius, gravity
one-oh-five-eight, smell is heavy on the Citra and Mosaic, krausen
dropped about a centimetre overnight, going to push to twenty for
the diacetyl rest tomorrow". The app updates the curve, predicts
FG 1.014 ± 0.003, and at packaging it cites batches v3, v6, v7
and proposes one tweak for v9: "dry-hop on day six instead of day
seven — v6 and v7 both showed sharper hop expression when the
dry-hop went in during active fermentation".
- **Czech pilsner in Manchester, brewed cold.** A Czech-British
brewer in Manchester is iterating on a Czech-style pilsner across
six batches. Day 1: "den jedna, jedenáct Celsius, OG jedna nula
čtyřicet osm" — code-switching between Czech and English. The
schema captures both. The tweak proposer cites v3 and v5: "v3
fermented at 9 °C and finished cleaner; v5 fermented at 11 °C and
had a faint diacetyl. Try 9 °C for v7 and lengthen primary by
three days".
- **A Vienna lager in Mexico City.** A homebrewer in Mexico City
has been chasing the Vienna lager his grandfather drank in
Orizaba. He records day 2 in Spanish: "día dos, doce grados,
gravedad uno-cero-cuatro-cinco, espuma hermosa, lleno de carácter
de malta". The app charts it. After batch v4 the tweak proposer
cites v2 ("OG was 1.052, finished 1.014, attenuation 73%") and
proposes raising mash temperature by 1 °C to retain more body for
v5.
- **A kveik pale brewed at 35 °C in a closet.** Voss kveik runs
hot. The user logs day 1 "thirty-five Celsius, krausen forming
vigorous", day 2 "thirty-six, vigorous", day 3 "thirty-five,
dropping, gravity one-oh-one-eight". The app's predicted FG
shrinks from a 1.014 ± 0.004 band on day 1 to 1.010 ± 0.001 on
day 3 — the brewer watches the band collapse as kveik delivers.
- **A wild fermentation across eighteen months.** A mixed-culture
brewer in Brussels is logging a lambic-style aged in oak. Day 1:
cool ambient, no pitched yeast. Week 2: Brett character emerging.
Month 3: pellicle visible on the surface. Month 8: gravity
refused to move from 1.012 for three weeks before crashing to
1.004 on week 35. The schema and the chart handle the long
timeline (days as the x-axis, log-scaled to keep the early
rapid-change visible).
- **The diacetyl rest decision.** Mid-fermentation, a brewer
notices a butterscotch note in the airlock vapour at day 5. He
asks the app "is this diacetyl?". The off-flavour explainer
responds with a short paragraph and surfaces the disclaimer
("this is not a substitute for an experienced brewer's
judgement"). The tweak proposer flags
`food_safety_disclaimer_required: true`.
- **The infected batch question.** A brewer photographs a strange
white film on the surface of his beer. The photo-description
call describes it neutrally ("a thin off-white film with a
textured surface, slight wrinkling visible at the rim"); the
brewer asks "is this mould?". The off-flavour explainer responds
with a not-a-substitute disclaimer prominently surfaced and
suggests his local brewing club, the manufacturer's forum, or a
food-safety authority.
- **The side-by-side batch overlay.** Sunday morning, brewer is
deciding what to change for batch v9. He selects v6 and v7 and
the gravity curves overlay on one chart. v7's curve sits 1.5 SG
higher than v6's at every point. The overlay surfaces a
quantified summary: "v7 fermented 1.4 °C cooler through primary;
finished 1.5 SG higher; both batches used the same yeast pitch
rate."
- **The first share with a brewing-club mate.** A user shares
"Czech Pilsner v6" with a friend. The friend opens the link,
sees the recipe header, the gravity curve, the tasting notes, and
posts back a structured comment: "the Saaz aroma feels muted on
day 30 — did you dry-hop?" The comment thread is per-batch and
revocable.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy to
fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking shot of a garage
workbench at golden hour with a clear glass carboy of fermenting
hazy IPA, a hydrometer floating in a flask beside it, a
handwritten notebook open with smudged ink readings. One
paragraph: "Brewing Notebook is a per-batch fermentation journal
that listens. Record a voice note in the closet, the garage, or
the brewhouse, and we'll build the graph and propose the one
thing to tweak next time." Single Google sign-in button; Apple
sign-in next to it. Below: "Try with the sample notebook" →
loads the demo lineage in section 8a.
2. **Notebook home.** The user's batches grouped by style lineage.
Each lineage shows: style name, batch count, most recent batch
status (fermenting / conditioning / packaged), and a tiny
sparkline of the most recent gravity curve. Tap a lineage → list
of batches; tap a batch → detail view.
3. **Active batch — day-by-day log.** Top: recipe header (style,
OG target, yeast, vessel). Middle: the gravity-curve graph (full
width, measured points, dashed predicted curve to FG, shaded FG-
confidence band, temperature line on a secondary axis, hop
additions as vertical markers, dry-hop bags as labelled chips,
krausen notes as annotations). Bottom: chronological feed of
observations (each one a card with date, voice-memo waveform,
transcript, parsed fields, photos). One big red record button
floats above the feed.
4. **Voice-recording flow.** Tap record → live waveform with a
subtle gain meter. Stop → a 3-line preview shows the parsed
fields ("day 4, 19 °C, SG 1.058") with a confirm button.
Confirm → the observation drops into the feed and the graph
updates with a small animation. Reject → the voice memo is
preserved but unparsed; the user can edit the parsed fields
manually.
5. **Side-by-side overlay.** Pick two or more batches in one
lineage → the gravity curves overlay on one chart. A
quantified summary below the chart names the differences
("v7 fermented 1.4 °C warmer through primary; finished 1.5 SG
higher").
6. **Tweak proposal view.** When the brewer marks a batch as
"packaged" or "ready to plan next batch", the tweak proposer
call runs and surfaces the single concrete tweak. The proposal
card shows: the tweak one-liner in large type, the rationale in
prose (3-6 sentences citing batches with chips that link to
them), the expected effect, the confidence as a slim bar, the
risk-note below in muted type. The food-safety disclaimer
surfaces prominently when the proposal touches anything
sanitation- or off-flavour-adjacent.
7. **Recipe editor.** A focused form for the recipe header. Style
picker (BJCP 2021 categories), batch size, OG/FG/IBU/SRM/ABV
targets, mash schedule, grain bill, hop schedule, yeast strain
(with attenuation and temperature ranges resolved via the
grounding call), water profile, salt additions, parent-recipe
link. BeerXML / BeerJSON 2 import on the side.
8. **Brewing-day mode.** Full-screen kettle-side checklist: strike
water temperature, mash-in, vorlauf, sparge, pre-boil gravity,
boil start, hop additions countdown (live, audible if user
chooses), whirlpool, chill, transfer. Each step gets a
timestamp and an optional voice note. Designed for a wet hand
and a hot kettle — large tap targets, high contrast, no fine
print.
9. **Off-flavour explainer.** Each off-flavour name in the schema
has its own page. Plain-English description, common causes,
common fixes, food-safety disclaimer where applicable. Each
instance the brewer flagged in his history links here for
re-reference.
10. **Style lineage history.** The full chronological list of
batches in one style, with key parameters as columns and a
multi-line chart of OG, FG, and ABV across the lineage. The
brewer can see his iteration arc at a glance.
11. **Sharing.** Modal: "Share this batch with a brewing-club mate
or another brewer." Magic-link email; the recipient lands on a
read-only view of the batch with a comment field. Per-batch,
revocable.
12. **Settings & privacy.** Unit preferences (SG vs Plato; Celsius
vs Fahrenheit; litres vs gallons), notification preferences
(calendar-style local reminders only), export to BeerXML /
BeerJSON, "Delete this lineage" / "Delete this account"
controls with a 60-second cool-off. Privacy panel restates the
not-trained-on policy in plain English.
13. **Footer.** "Made for the brewer dialling in batch v8 of v8."
Privacy: "Your recipes, voice memos, and batch data are yours.
We never train on them." Capabilities `(i)` icon in header.
## 6b. First-visit onboarding
Show a **first-visit onboarding** the first time a visitor lands on
the app (detect via `localStorage` flag; do not show on return
visits). Three slides, dismissible at any time. Persistent
re-entry: a `?` icon in the header reopens it.
**Slide 1 — What this is.**
- Headline: "Welcome to Brewing Notebook."
- Subhead: "A per-batch fermentation journal that listens — in any
style, any scale, any vessel — and tells you the one thing to
tweak next time."
- One paragraph (≤ 60 words) explaining who this is for and what
makes it different from a generic brewing app: voice is the
primary input, the graph is the working surface, the long-context
model reads the whole lineage to propose exactly one concrete
tweak per batch, and food-safety adjacent advice is always
flagged as not-a-substitute.
- Visual: a small annotated illustration of a gravity-curve graph
with the relevant features labelled (measured points, predicted-
FG band, hop addition markers, krausen note annotations) — not
a generic beer mug.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample notebook".
- A live demo input pre-loaded with the eight-batch hazy IPA
lineage from the seed content in section 8a.
- 1-2 sentences pointing at *the specific page elements* where the
Gemini magic happens (the voice-parse on the day-4 memo, the
predicted FG band, the long-context tweak proposal for batch v9).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample lineage in `/data/seed-notebook/` for your own
batches."
- "Adjust the prompts in `/server/prompts/` to fit your style and
vocabulary (cider, mead, kombucha, sake)."
- "Wire up your Gemini API key and Firebase project via the
env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build
remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag,
never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`,
`aria-modal="true"`, `aria-labelledby`, focus restored to trigger on
close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath must be
fully usable.
- Don't auto-reshow on return visits. Use
`localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social follow).
Keep it about the template only.
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header (next to
the primary nav). Click → opens a modal/panel titled **"What powers
this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini 3.5 Flash (multimodal audio)** — listens to your voice
memos in any language and parses them into structured
observations: day, temperature, gravity reading, hop additions,
krausen notes, smell descriptors, off-flavour flags, decisions
taken. Code-switching between languages mid-memo is supported.
- **Gemini 3.5 Flash (multimodal image)** — describes your wort,
krausen, hydrometer, and packaging photographs into the
observation log. Useful months later when the long-context tweak
proposer reads your lineage.
- **Gemini 3.5 Flash (long context)** — reads your whole style lineage
at once (up to ~100 batches before chunking) to propose exactly
one concrete, single-variable tweak for your next batch, anchored
in citations from prior batches.
- **Gemini 3.5 Flash (structured output)** — every voice memo becomes a
typed `BatchObservation`; every tweak proposal is a typed
`BatchTweakProposal`. The schemas live in the repo.
- **Gemini 3.5 Flash + grounded search** — resolves yeast attenuation
ranges, hop variety descriptors, water-chemistry standards, and
BJCP 2021 style targets from cited public sources.
- **Gemini TTS** — reads your past voice memos aloud when your
hands are wet at the kettle.
- **Nano Banana 2** — generates the welcome and empty-state imagery
(photographic-looking carboys, kettles, workbenches).
- **Firebase Auth** — Google and Apple sign-in; per-batch sharing
via magic links.
- **Firestore** — stores your batches, syncs across devices in real
time.
- **Firebase Storage** — keeps your original voice memos and
photographs at upload quality, forever.
- **Cost note** — see the detailed breakdown in 6d. A single
brewing day with 6 observations costs about $0.08 of Gemini API
spend. A full lineage of 10 batches over a year of dialling in
costs about $1.10 of Gemini total.
- **Privacy note** — your recipes, voice memos, and batch data are
private to you and any brewer you explicitly share a batch with.
This app uses the Gemini API on the paid tier, where Google does
not use your content for model training, per the Gemini API
Additional Terms.
- **Food-safety note** — this app charts, suggests, and warns; it
does NOT give food-safety advice as if it were authoritative.
Whenever a question touches sanitation, contamination, or
fermentation safety, the answer is flagged as not a substitute
for an experienced brewer's judgement, your local brewing club,
or your local food-safety authority.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b — REQUIRES manual enable in Firebase
console; AIS Build does not auto-provision Storage today.
- Email: see section 4b — magic-link sharing requires the sender
domain to be authorised in Firebase Auth.
- Apple sign-in: optional, requires an Apple Developer account and
Service-ID config. See section 4b.
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b
**Environment variables you'll need to configure:**
- `GEMINI_API_KEY` — your Google AI Studio API key
- `FIREBASE_PROJECT_ID` — your Firebase project id
- `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side
only)
- `OPENWEATHER_API_KEY` — optional, only if you want ambient-
temperature annotation for outdoor / non-temperature-controlled
brewing
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: long-context
tweak proposer calls are billed per token of input — a 10-batch
lineage call is about $0.08 per run (default: at packaging time
only).
- One short paragraph on privacy: where the data lives (your
Firebase project), how to delete it (Settings → "Delete this
lineage forever" — gone in 60 seconds), what is never sent for
training.
**Documentation links:**
- AI Studio Build docs
- Gemini API multimodal audio, multimodal image, long-context,
TTS, grounded search docs
- Firebase Auth, Firestore, Firebase Storage docs
- BJCP 2021 style guidelines (for style-target overlays)
- BeerJSON 2 spec (for recipe import / export)
**Accessibility:** same standards as the onboarding modal — focus
trap, `Esc`, ARIA, restored focus.
**Behaviour:**
- Always available — single click from anywhere in the app.
- Tooltip on the `(i)` icon: "How this app is built".
- Mobile: opens as a full-screen sheet that slides up.
- Should be the most honest part of the app — never hand-wave
service requirements; never say "AI" without naming the specific
Gemini model and capability.
## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping)
- **Voice memo → BatchObservation (Gemini 3.5 Flash, low thinking)** —
a 30-second voice memo plus the system instruction averages
~3,000 input tokens and ~600 output tokens. At Gemini 3.5 Flash
pricing (~$1.50/M input, ~$9/M output) that is ~$0.0063 per
voice memo. A typical brewing day has 6 observations → ~$0.038.
- **Photo description (Gemini 3.5 Flash, low thinking)** — ~1 image +
short system instruction, ~1,200 input tokens and ~200 output
tokens → ~$0.0025 per photo. ~3 photos per batch on average →
~$0.0075/batch.
- **Predicted FG sanity-check (Gemini 3.5 Flash, low thinking)** —
small input (recipe + last few observations), ~1,500 input
tokens and ~300 output tokens → ~$0.0034 per run. Run once per
new observation → ~$0.020 per brewing day.
- **Long-context tweak proposer (Gemini 3.5 Flash, medium thinking,
long-context)** — a 10-batch lineage averages ~60,000 input
tokens and ~600 output tokens → ~$0.078 per run. Default
frequency: run once at packaging time → ~$0.08 per batch.
- **Grounded fact call (Gemini 3.5 Flash + grounded search)** —
small input, ~$0.0008 per call. Used a handful of times per
recipe (resolving yeast attenuation, hop alpha-acid range,
style targets) → ~$0.005/batch.
- **Off-flavour explainer (Gemini 3.5 Flash)** — ~$0.0005/call;
user-driven, a few calls per batch at most.
- **TTS playback (Gemini 2.5 Flash TTS)** — ~$10/M output tokens
≈ ~$0.000003/character. A 90-word memo readback is ~$0.0017.
Cached per memo; charged once.
- **Hero / empty-state image (Nano Banana 2)** — ~$0.03/image.
Generated once per app install (cached client-side), or once
per regenerate-empty-state action.
- **Expected per-batch cost (across full lifecycle):** ~$0.18 —
~6 voice memos at ~$0.0063, ~3 photo descriptions at ~$0.0025,
~6 predicted-FG sanity checks at ~$0.0034, 1 long-context tweak
proposer at ~$0.08, a few grounded facts, a few off-flavour
explainers.
- **Year of dialling in one style (10 batches):** ~$1.80 total
Gemini API spend.
- **Voice memo storage:** Firebase Storage standard tier ~$0.026/
GB/month. A 30-second voice memo at 64 kbps is ~240 KB; 60 memos
per batch year ≈ ~14 MB ≈ ~$0.0004/month. Photos add another
~50 MB/batch ≈ ~$0.001/month per batch.
## 7. Design language
- **Mood:** A brewer's working surface, not a tech product. Not a
Untappd-style social feed. Not a SaaS dashboard. The garage at
9 pm with the airlock bubbling, the carboy in the closet, the
notebook on the kitchen counter beside the kettle still cooling
down, the tablet on a stand in the brewhouse beside the conical
fermenter. A workbench at the moment between batches.
- **Typography:** Clean grotesque for app chrome and data labels
(Inter or Geist). Display serif for the batch name and lineage
heading (Source Serif Pro or Fraunces). A handwriting-styled
accent (sparingly) for the user's own voice-memo transcript
preview — to mark the difference between the brewer's words and
the structured fields parsed from them.
- **Palette:** Warm paper background `#F4EFE6` for the notebook
surface, deep ink `#1B1714` for body text, malt amber `#A37430`
for headings and graph axes, wort gold `#D9A646` for the
predicted-FG band, hop green `#5B8C3A` for hop-addition markers,
diacetyl butter `#E0C065` only for off-flavour flags, faded red
`#A33A2C` for the food-safety disclaimer chrome and for the
one-tweak proposal card border. Borrowed from a brewer's
notebook, not from SaaS design systems.
- **Imagery:** Photographic. Carboys in real garages at golden
hour. Stainless conicals in small brewhouses. Notebooks open
beside hydrometers on wooden workbenches. No flat illustrations.
No beer-mug emoji. Generated via Nano Banana 2 with prompts
emphasising real working surfaces, warm light, no people, no
commercial branding.
- **Hand-feel touches:** Each new observation drops into the feed
with a thin paper-shadow as if a notebook page were turning. The
gravity-curve graph's predicted-FG band has a soft watercolour-
wash quality — it is a range, and it looks like a range.
Hovering on a measured point reveals the verbatim voice-memo
transcript that produced it. The hop-addition markers rise out
of the chart with a small lift, as if pegged on a real cork
board.
- **Spacing:** consistent 4-px base. Generous whitespace — the
graph and the feed need air.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px).
Observation cards use 6; the tweak-proposal card uses 12; the
welcome card uses 20.
- **Shadows:** subtle, layered, warm-tinted. Avoid heavy drop-
shadows.
- **Motion:** purposeful — entrance fades, hover lifts, page
transitions. Respect `prefers-reduced-motion`. The
predicted-FG band's "shrinking on new data" animation is the
one place where motion carries meaning; respect reduced-motion
by jumping instead of animating. No bouncing splash animations.
No theatrical hero animations.
- **States:** every interactive element has hover, focus, active,
disabled. Loading uses skeletons not spinners where possible.
Empty states have helpful next-action guidance ("Record your
first day-1 voice note to start the curve").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic
placeholders like 'Your tagline here'.
- Invent plausible names, batch numbers, voice-memo transcripts,
gravity readings, and tweak proposals that fit the domain (use
the seed content in section 8a as a starting point). When
inventing, lean on realistic brewing patterns — hazy IPA
finishing 1.014, kveik attenuating 78%, Czech pilsner
fermenting at 9 °C — but never claim that a fictional batch
came from a real brewery or that a fictional yeast measurement
is a real published value.
- Tone: warm, direct, free of corporate language. This template
is for a brewer, not a brewing company.
- Headlines: punchy and concrete. No 'Empower your X' filler. No
'Revolutionize'. No 'Seamless'. No 'AI-powered'.
- Body copy: short paragraphs (2-4 sentences). Use lists where
appropriate.
- Plain language. Avoid jargon — except where the brewer already
speaks the jargon (the home brewer wants to see "SG", "Plato",
"IBU", "dry-hop", "diacetyl rest" because they speak that
vocabulary daily).
- Where the app outputs AI-generated content, never label it as
"AI says" — let it speak naturally. Use small uncertainty cues
only where epistemic honesty requires them (a low-confidence
gravity reading shows as faintly underlined; tapping reveals
the alternate units the model considered).
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete
content below. Use these names, numbers, dates, and snippets
verbatim where helpful, or generate close variants that sit in the
same world.
**Sample lineages (sidebar):**
- "Hazy v" (8 batches, just me) — primary lineage: a hazy IPA
dialled across eight batches in a Daly City garage. OG 1.060–
1.068, FG targets around 1.014, yeast London Ale III at 19 °C,
hops Citra + Mosaic + Nelson Sauvin in various proportions.
- "Czech Pilsner" (6 batches, contributors: me, club mate
Honza in Manchester) — bohemian pilsner across six batches at
9–11 °C in a converted chest freezer with an Inkbird controller.
- "Vienna Lager — Orizaba style" (4 batches, just me) — a Vienna
lager built to resemble the import-amber style brewer's
grandfather drank in Orizaba.
- "Voss Kveik Pale" (5 batches, just me) — a pale ale with Voss
kveik at 35 °C in a hall closet. Two-day primary, packaged in
ten.
- "Kettle Sour — Berliner Weisse" (3 batches, just me) — a
Berliner Weisse using a Lactobacillus kettle-souring stage
before the boil.
**Sample batch in detail view (this is what the demo should show):**
- **Batch name:** "Hazy v8"
- **Style:** American IPA – Hazy/NEIPA (BJCP 21B)
- **Batch size:** 19 L
- **OG measured:** 1.062
- **FG target:** 1.014
- **IBU target:** 50
- **SRM target:** 5
- **ABV target:** 6.3%
- **Yeast:** London Ale III (Imperial A38 Juice), published
attenuation 70–75%, published temperature range 17–22 °C
- **Grain bill:** 78% 2-row Pale Malt, 12% Flaked Oats, 6%
Wheat Malt, 4% Carapils
- **Hop schedule:** 14 g Citra at whirlpool (15 min, 80 °C);
14 g Mosaic at whirlpool; 30 g Citra + 30 g Mosaic dry-hop
during active fermentation, day 3; 30 g Nelson Sauvin dry-
hop on day 7
- **Water profile:** "soft NEIPA" — 4 g calcium chloride and
2 g gypsum into 25 L total brewing water
- **Observation v8-day-1 (verbatim voice memo):** "day one,
twenty Celsius, pitch is happy, krausen forming, smell is
mostly worty, going to drop to nineteen tonight"
- **Observation v8-day-3 (verbatim voice memo):** "day three,
nineteen Celsius, gravity one-oh-three-eight by refractometer
uncorrected, krausen peaked, smell starting to show Citra,
dropping the first dry-hop bag now, sixty grams Citra and
Mosaic"
- **Observation v8-day-4 (verbatim voice memo):** "day four,
nineteen Celsius, gravity one-oh-five-eight measured this
morning… wait, I meant one-oh-three-zero, sorry, by hydrometer
this time, krausen has dropped about a centimetre overnight,
going to push to twenty for the diacetyl rest tomorrow"
- **Predicted FG (after day-4 observation):** 1.014 ± 0.003, SG;
confidence 0.68; rationale: "gravity has dropped 0.032 SG since
day 1, consistent with the day-4 readings on v6 and v7 at the
same temperature; London Ale III's published 70–75%
attenuation range bounds the expected terminal gravity to
1.0124 – 1.0156."
- **Tweak proposal (one-line):** "Drop the first dry-hop bag on
day 6 instead of day 3 for v9."
- **Tweak proposal (full rationale):** "Batches v3 and v6 had
the same dry-hop schedule as v8 (day 3 + day 7) and finished
with muted hop aroma at packaging; v7 dropped the first dry-
hop bag on day 6 with the gravity at 1.020 and showed a
noticeably brighter Citra expression at packaging. Three
batches in this lineage (v3, v6, v7) are enough evidence to
pin the dry-hop timing to active-fermentation completion
rather than calendar day. v9 should drop the first dry-hop
bag when the gravity hits 1.020 (estimated day 6 at 19 °C)
and the second bag at day 9 instead of day 7."
- **Cited batches:** v3, v6, v7
- **Expected effect:** "Brighter Citra and Mosaic aroma at
packaging; should not affect final gravity by more than
0.001 SG."
- **Confidence:** 0.74
- **Risk note:** "May reduce hop bitterness perception slightly;
the v7 packaging notes mentioned a slightly leaner finish."
- **Food-safety flag:** false
- **Food-safety disclaimer required:** false
**Sample voice memos with multilingual code-switching:**
- Honza (Czech-British, Manchester): "den jedna, jedenáct
Celsius, OG one-oh-four-eight, pitched two packets of W-34/70,
krausen forming nicely, smell is malty and clean — language
Czech with English numeric switching"
- Maricel (Filipino-American, Daly City): "day four, nineteen
na lang, gravity one-oh-three-zero, krausen peaked yesterday,
may amoy ng Citra, going to drop dry-hop in two hours — Tagalog
with English brewing vocabulary"
- Diego (Mexican, Mexico City): "día dos, doce grados, gravedad
uno-cero-cuatro-cinco, espuma hermosa, lleno de carácter de
malta — Spanish throughout"
- Solomon (Ethiopian-American, Oakland): "day one, twenty-one
Celsius, OG one-oh-five-four, የጥራጥሬ rich flavour, krausen
forming — English with Amharic descriptors"
- Mai (Vietnamese-Australian, Melbourne): "ngày bốn, mười chín
độ, gravity một-không-ba-mươi, dry-hop today — Vietnamese
with English brewing terms"
**Sample input artefacts (for the build to demonstrate):**
- A voice memo recorded in a garage with a humming dehumidifier
in the background, day 4 of Hazy v8.
- A photograph of a clear glass carboy with active krausen at
4 cm height, light hop-trub ring around the rim.
- A photograph of a refractometer showing approximately 1.030 on
the SG scale (uncorrected), hazy wort drop on the prism.
- A photograph of a stainless conical fermenter in a brewhouse
with a sample valve and a thermometer reading 19.2 °C.
- A photograph of a strange off-white film on the surface of a
beer (the brewer is asking "mould or pellicle?"; the app
describes neutrally and surfaces the not-a-substitute
disclaimer).
**Sample voice copy:**
- Onboarding: "Record one voice note from your fermenter. We'll
build the graph."
- Empty active batch: "This batch is waiting for its first day-1
voice note. Tap record."
- Processing: "Listening to your memo…" / "Parsing day, gravity,
temperature…" / "Updating the graph…" / "Reading your lineage
for the next tweak…"
- Save confirmation: "Added to Hazy v8 — day 4 observation, SG
1.030, temperature 19 °C."
- Predicted FG card: "Predicted final gravity: 1.014 ± 0.003.
This is the model's best estimate, not a guarantee."
- Tweak card heading: "One tweak for v9 — drop the first dry-hop
bag on day 6 instead of day 3."
- Food-safety surfacing: "This question touches on fermentation
safety. This app is not a substitute for an experienced
brewer's judgement, your local brewing club, or your local
food-safety authority."
- Low-confidence note: "We weren't sure about the gravity unit
here. Tap the underlined value to see what we considered."
- Share email subject: "Honza — can you look at Czech Pilsner v6
and tell me if I'm chasing a ghost?"
- Share email body: "Hey Honza — v6 finished at 1.014 again, same
as v5. I think the mash schedule is off. Have a look and ping
me back with your read. [Open Batch]"
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of
a garage workbench at golden hour, a clear glass carboy of
fermenting hazy IPA in the centre with visible active krausen,
a refractometer and a paper notebook open beside it, warm
evening light. Generate via Nano Banana 2 with a prompt
emphasising "warm afternoon light through a garage window, no
people in frame, a clear glass carboy of fermenting hazy IPA
with active krausen, a refractometer on a wooden workbench
beside it, a paper notebook open with handwritten gravity
readings, real worn paper, soft shadow under the carboy".
- **App icon / wordmark:** Set in the display serif. Slightly
worn paper texture behind it. No icon — just type.
- **Empty-state illustration:** A simple line drawing of a single
carboy with an airlock on top. Hand-drawn aesthetic, not a flat
icon.
- **Demo photos:** Generated per the prompts in section 8a —
Nano Banana 2 prompts that specifically request "stainless
conical fermenter with sample valve, soft overhead light",
"hydrometer in a sample tube, hazy gold liquid, focus on the SG
scale reading approximately 1.030", "wort at flameout in a
stainless kettle, deep amber colour, slight steam". Each demo
photo should look photographed, not rendered.
- **Style-target overlay graphics:** Vector overlays for BJCP
2021 ranges on the gravity chart (shaded regions for OG, FG,
IBU, SRM, ABV).
- **Stock fallbacks:** If image generation fails, fall back to
the photographed sample carboy from `/public/samples/sample-
carboy.jpg`. Never to a "🍺" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock
photography. Prompt for warmth, asymmetry, and slight
imperfection — avoid the glossy 'AI render' look.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit
`width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI. Use sparingly — never
decorative-only.
### Build-time asset manifest (explicit specs)
Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs.
**Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id:
- `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted.
- `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence.
- `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible.
**Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill):
- `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt.
- `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline.
- `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails.
**Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint.
**Hard rules**
- Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error.
- No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code.
- Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`).
- File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and disabled
states.
- Forms validate inline and show specific error messages (not
"Invalid input"). "Gravity reading 0.058 looks low — did you
mean 1.058?" is the right shape.
- Loading states use skeletons that match the eventual layout,
not spinners.
- Empty states explain the next action with a button whose label
fits THIS app's domain: "Record your first day-1 voice note",
"Import a BeerJSON 2 recipe", "Add another batch to this
lineage" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- All AI-generated content streams in token-by-token where
supported, with a clear "thinking…" indicator before content
starts arriving.
- If an AI call fails, show a calm, specific error ("We couldn't
parse the audio — the dehumidifier was loud. Want to try a
shorter clip, or type the day and gravity into the side
panel?") and offer retry.
- Low-confidence gravity readings in the chart are faintly
underlined; tapping reveals the alternate units (SG vs Plato vs
Brix) the model considered.
- The predicted-FG band's "shrinking on new data" animation
takes 400 ms with `prefers-reduced-motion` falling back to
instant.
- The food-safety disclaimer surfaces as a non-dismissable
inline banner above any tweak proposal that touches sanitation
or off-flavour control — not a toast that scrolls off-screen.
## 11. Tech & responsive requirements
- **TTS markdown-stripping preprocessor:** before sending any user-authored markdown to `gemini-3.1-flash-tts-preview`, strip non-spoken markdown: `#`/`##`/`###` headings (keep the title text), `**bold**` (keep the inner text), `[label](url)` (keep `label`, drop URL), `` ``` `` fenced code blocks (skip entirely), `>` block-quote markers (keep the text), and `|` table pipes (read row-by-row as sentences). Insert `…` between sentences for a short pause and a blank line plus `—` between paragraphs for a long pause. The model does not understand markdown; raw markdown will be read aloud as literal characters ("asterisk asterisk").
- **File downloads on Safari / Firefox:** when offering local-disk save of any export (PDF, CSV, MP3, ZIP, JSON, image), fall back to `` with a blob URL — the File System Access API (`showSaveFilePicker()`) is Chromium-only. Detect with `'showSaveFilePicker' in window`; otherwise use the anchor-download path.
- **Stack:** React + TypeScript + Tailwind CSS. Functional
components + hooks. Use Shadcn UI primitives where appropriate.
Graph rendering via Recharts or Visx (both work; Visx gives
more control over the shrinking-confidence-band animation).
- **Build runtime:** AI Studio Build — full-stack with Cloud Run
server-side functions. All Gemini API calls happen server-side;
API key lives in Secrets Manager, never in client bundle.
- **Model selection:** explicitly pin `gemini-3.5-flash` for voice-
parse / photo-describe / predicted-FG / tweak-proposer and
`gemini-3.5-flash` for grounded facts and off-flavour explainer.
Set `thinkingLevel` explicitly per call.
- **Database:** Firestore (auto-provisioned by AI Studio Build).
Show the seed lineage on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple
sign-in next to it; magic-link email as fallback (for share
links).
- **Storage:** Firebase Storage for voice memos and photos.
Pre-signed URLs only.
- **Audio handling:** the web Recorder API for voice capture;
capture as `audio/webm` or `audio/mp4` (depending on browser
support), upload to Firebase Storage, then pass the resulting
`files/*` resource name to the voice-parse Gemini call via the Files API.
Do NOT pass Firebase Storage public URLs to `generateContent`
— the API does not fetch them server-side.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px
(iPad), 1024 px, 1440 px+.
- Use `clamp()` for fluid typography. Prefer container queries
over media queries for component-level responsiveness.
- Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on
iOS.
- Zero horizontal overflow at any width. Zero layout shift on
load.
- Persist user data in Firestore. Use real-time listeners on the
active batch view (so a brewer recording in the closet sees the
graph update on the tablet in the kitchen).
- Optimistic UI on writes; reconcile on response.
- Brewing-day mode uses the screen-wake-lock API to keep the
display on at the kettle; falls back gracefully if unsupported.
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`,
`footer`.
- All interactive controls reachable by keyboard with a visible
focus ring.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI
components.
- All images have meaningful `alt` text. The photographed
observation images have `alt` describing the artefact
("photograph of a hydrometer reading approximately 1.030 SG in
hazy gold wort, taken in a Daly City garage at golden hour").
- Form fields have associated `