# 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.
---
# Read This To Me
## 1. Project
**Read This To Me** turns any long article, essay, newsletter, paper,
PDF, or web page into expressive, walk-paced narration you can listen
to while doing dishes, commuting, walking the dog, or sitting on a
train with the phone face-down. Paste a URL or drop a PDF, tap play,
and within three seconds a natural human-feeling voice — warm,
focused, or casual — is reading the piece out loud at a comfortable
walking pace of about 130 words per minute, with subtle emphasis on
the load-bearing sentences, slightly different timbre on direct
quotes, and natural breath at every paragraph break. Every word is
read; nothing is skipped or paraphrased. The player locks the phone,
keeps playing in your pocket, and remembers exactly where you stopped.
This is the kind of app a commuter wants on the morning walk to the
station — they saved a 6,000-word longform piece in a "read later"
tab three weeks ago and have never opened it. It is the kind of app a
PhD student wants when they have a 40-page paper to read before a
meeting at three and would rather hear it on a walk than skim it on a
screen. It is the kind of app a parent wants on Sunday afternoon when
the dishes need doing and there is a newsletter sitting in the inbox
they actually want to hear. Same job, three different scales of
attention. The app handles all three.
The single demo that proves the magic: a visitor pastes the URL of a
4,000-word essay, taps "Read this to me", and within three seconds
the narration starts. The voice is warm and unhurried. At the end of
each paragraph there is a real breath. Quoted dialogue lands with a
slightly different vocal colour so the listener feels the speaker
change without being told. The key claim of the second section gets
a faint emphasis the listener notices but cannot quite name. Chapter
markers appear at every heading; one tap skips ahead. The lock-screen
shows a clean cover, the article title, and play / pause / skip
controls. The phone goes in the pocket and the listener walks.
In the harder cases — a 78-page PDF research paper with figures and
footnotes, a newsletter with embedded code blocks the listener does
not want read out, a long-form interview where two speakers alternate
— the app reads the long arc too: pages are stitched cleanly across
column breaks, footnotes get a one-line "footnote 14, three sentences,
tap to expand" mention rather than a full read-through that derails
the flow, code blocks are skipped with a "code block, twelve lines,
tap to read" handoff, and the two speakers in an interview get two
distinguishable voice timbres so the listener can follow who is
talking.
**Tagline:** _Paste a URL, drop a PDF. Walk away with the essay read
to you — at walking pace, in a real human-feeling voice, every word._
## 2. Target audience
- Commuters and walkers who already have a stack of saved-for-later
longform pieces and never make it back to the tab
- PhD students, lawyers, policy analysts, and anyone whose inbox
fills with PDFs they should read on the train but read poorly on a
screen
- Parents doing dishes, folding laundry, walking the dog — listening
is the only time of day they actually have to read
- People with dyslexia, post-concussion symptoms, eye strain,
migraine, or visual fatigue for whom an audio companion to written
longform is genuinely accessible
- Newsletter readers who subscribe to ten weekly essays and never get
through more than two; the audio version cuts through
- Researchers who want to "read" a paper while running and form an
opinion before the lab meeting
- Listeners on flights, in airports, in long car queues, on the bus —
anywhere the eyes are taxed but the ears are free
- People who simply prefer audio — they would buy an audiobook
version of every essay if such a thing existed, and find paid
per-article narration services (which exist but stop reading after
one weekly piece) too restrictive
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section
ordering — they are the reasons users pick this app.
- **Expressive voice, not robotic TTS** — the post-I/O 2026 expressive
TTS model (`gemini-3.1-flash-tts-preview`) produces narration with
real prosody: natural breath at paragraph breaks, subtle emphasis on
the sentence the author is leaning on, a different vocal colour for
quoted material, and a pace that matches how a friend would read
the piece aloud — not the flat, equal-stress cadence of stock TTS.
- **Every word, no shortcuts** — the app NEVER skips or paraphrases.
Summarisation is a separate, opt-in mode; the default contract is
"every word the author wrote, in order, read out loud". This is the
load-bearing trust contract — a listener who has been burned by
summaries that skipped the part they wanted to hear can rely on this
app to read the whole piece.
- **Walking pace by default** — narration runs at ~130 words per
minute by default, the pace at which a thoughtful friend reads a
piece aloud on a walk. Adjustable from 100 wpm (close-listen) to
180 wpm (fast catch-up); the default is tuned for ambient listening
while moving. The pace knob lives on the player face, not in a
settings page.
- **Three voices, three jobs** — _Warm_ for personal essays,
newsletters, memoir; _Focused_ for research papers, technical
pieces, policy writing; _Casual_ for blog posts, light writing,
conversational longform. The voice change is conveyed via a single
style-directive sentence prepended to the input (Gemini 3.1 Flash
TTS does not support SSML), and the listener picks it once per
piece via a one-tap chooser.
- **Reading-aware pause structure** — paragraph breaks become real
pauses; sentence boundaries become micro-pauses. Encoded as the
textual cues `…` for sentence pauses and a blank line plus `—` for
paragraph pauses (since Gemini 3.1 TTS has no SSML `` tag).
Headings get a short tonal lift; quotations get a slightly more
intimate timbre. Foot-of-page and end notes are flagged inline
rather than read full-bleed.
- **Offline-cached playback** — once the narration has been generated,
the MP3 is cached on-device so the user can listen on a flight,
underground, or in dead-zone wilderness. The download chip on the
player face shows MB size and percent cached.
- **Background-audio first-class** — the app implements the Media
Session API with full lock-screen art, scrubbing, play/pause, skip
forward / back by chapter, and per-15s seek. The phone screen
locks and the audio keeps playing. The user can answer a call, take
a photo, or run another app and come back without losing position.
- **Honest about what gets skipped** — code blocks, large tables,
raw equations, and figures that do not render as prose are
surfaced as one-line spoken handoffs ("code block, twelve lines,
tap to read") rather than awkwardly narrated. The listener decides
whether to break narration flow to engage with them, and a
per-piece toggle "read everything verbatim, even code" exists for
the listener who wants no exceptions.
## 4. Features to build
- Paste-URL flow — single text input on the home screen accepts a URL
to an article, a Substack post, a Medium post, a personal-blog page,
a Wikipedia article, an arXiv preprint page, a news article. The
fetcher resolves the canonical reading view (boilerplate stripped)
before passing prose to the TTS pipeline.
- Drop-PDF flow — drag-and-drop a PDF, or tap to upload from the
device; PDFs up to 200 pages handled in a single pipeline pass with
multimodal page-parsing on Gemini 3.5 Flash.
- Share-extension entry (iOS Share Sheet / Android Share intent / web
Web Share Target) — share any URL from any app and the piece is
queued and ready to play before the user opens the app.
- Player face — large cover art (generated or extracted), title,
author, estimated runtime, voice picker, pace knob, chapter list,
scrubbing timeline, play/pause/skip-15/back-15/skip-chapter/
back-chapter, sleep timer.
- Voice picker (one tap, three options) — Warm / Focused / Casual,
with a 6-second preview clip of each reading the article's opening
line so the listener picks the right tone for the piece.
- Chapter skip — headings (`
`–`
` for HTML; outline tree for
PDFs) become chapter markers. The listener can jump between
chapters with one tap.
- Highlight-emphasis pipeline — Gemini 3.5 Flash reads the prose
before TTS generation and emits a JSON map of `paragraph_index →
emphasis_directive`. Directives are encoded as inline textual cues
the expressive TTS model honours (italics-equivalent slight slow,
inline emphasis on a single phrase via short pre-pause). Never more
than 1-2 emphasis cues per paragraph.
- Quote-aware voicing — quoted material inside the prose is detected
by the prose-analyser call and rendered with a slight tonal shift
by prepending a short directive sentence to the TTS input for that
segment (e.g. "the next sentence is a direct quotation; read it
with a slightly more intimate timbre"). For interviews with two
speakers, the second speaker's segments get a different voice
selection within the same family.
- Reading-position persistence — the player remembers exactly where
the listener stopped, to the second, across sessions and devices.
- Offline cache — generated MP3s saved on-device (IndexedDB on web,
on-disk on mobile). Cache chip shows size and "downloaded" state.
- Background audio — Media Session API, lock-screen controls,
Bluetooth headphone controls, Airpods double-tap, CarPlay /
Android Auto when available.
- Sleep timer — 5 / 10 / 20 / 30 minutes or "end of chapter".
- Search-within-piece — full-text search over the parsed prose;
tapping a match plays from that sentence.
- Listening queue — a simple linear queue of pieces; auto-advance to
the next piece when the current one finishes.
- Listening history — what was listened to, when, how much. Local
only; never sent to a server unless the user explicitly turns on
cross-device sync.
- Per-piece "read everything verbatim" toggle — for listeners who
want zero handoffs (every code block, table, footnote read in
full).
- Per-piece "skip footnotes" toggle — for listeners who want a
cleaner narrative pass; the footnotes are listed at the end and
can be played as a single trailing segment.
- Article-cover generation — Nano Banana 2 generates a clean cover
image for the piece based on the title and the first paragraph,
used in lock-screen art and in the queue list.
- Cross-device sync (opt-in) — Firestore stores the listener's
position and queue, syncs across devices when the user signs in
with Google or Apple. Off by default.
- Personal-feed import — connect an RSS reader (Feedly, Inoreader,
generic OPML upload) and saved-for-later items appear as a
listening queue.
- One-tap "send to my phone" from a web session — when the listener
pastes a URL on desktop and wants to listen on the walk home, a
signed-in user can push the piece to their phone with one tap
(Firestore real-time listener; no notifications needed).
## 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)
- **Expressive TTS** (`gemini-3.1-flash-tts-preview`) — the
load-bearing hero. Generates expressive, prosody-rich narration
from plain prose at ~24 kHz output. Style is steered by a single
one-sentence directive prepended to the input ("Read this with a
warm, unhurried voice, like reading a friend's essay aloud on a
walk. …"); there is no SSML in Gemini 3.1 Flash TTS — pauses are
encoded as `…` for sentence breaks and blank-line `—` for paragraph
breaks. The model produces noticeably more natural prosody than the
prior `gemini-3.1-flash-tts-preview` (released 2026-04-15;
pre-I/O 2026 templates used the older ID — replaced here).
- **Prose analyser + emphasis planner** (`gemini-3.5-flash`,
thinkingLevel `medium`) — reads the parsed prose of the whole
piece in a single 1M-context call and emits a structured
`NarrationPlan` that describes per-paragraph metadata: paragraph
type (body / heading / quote / footnote / code / table-handoff),
emphasis directives (at most 1-2 per paragraph), voice handoff
cues for multi-speaker interviews, chapter boundaries. **Guardrail**:
a 60-page PDF or 30,000-word longform can run to ~100k tokens of
prose; cap at 500k input tokens (well below the 1M ceiling) and if
the parsed prose exceeds that, chunk on top-level headings and
emit a per-chunk NarrationPlan, then concatenate.
- **URL / PDF prose extractor** (`gemini-3.5-flash`,
thinkingLevel `low`, multimodal image/PDF input) — accepts the
full PDF or fetched HTML and emits a structured `ParsedPiece`:
title, author, publication, date, ordered list of `Paragraph`
blocks with type and content. For PDFs the model reads page
images directly via the Files API; for HTML the server pre-strips
boilerplate (using a Readability-style heuristic) before sending
the residual prose. **Multipage / multi-image input** uses the
Gemini Developer API Files API (`files/*` resource name, e.g.
`files/abc123xyz`, passed via `fileData: { fileUri, mimeType }`)
for PDFs over 5MB; sub-5MB PDFs can be sent as `inlineData`
(base64). NEVER pass Firebase Storage public URLs to
`generateContent` — the API does not fetch them server-side.
`gs://` URIs belong to Vertex AI / Cloud Storage — a different
surface, not accepted on the Developer API.
- **Cover-art generator** (`gemini-3.1-flash-image`, Nano Banana 2) —
generates a clean cover image for each piece based on title and
first paragraph. Used in lock-screen art and queue thumbnails.
Reserved hero use of `gemini-3-pro-image` (Nano Banana Pro) for
the optional 4K poster export feature (see section 5).
- **Structured output / JSON Schema** — the prose extractor returns
the `ParsedPiece` schema; the analyser returns the `NarrationPlan`
schema. Both schemas are seeded verbatim in the system instruction
and in `responseSchema`. Convert the Zod schema to Gemini's
`Schema` type via the SDK helper before passing as
`responseSchema` — do NOT pass raw Zod. Numeric `min`/`max`
constraints in the schemas are documentation only; clamp on the
server after the response arrives.
- **Thinking levels** — `medium` for the analyser (it must reason
about emphasis, structure, speaker turns across the whole piece);
`low` for the prose extractor (mechanical structure extraction);
not applicable to TTS or image generation calls (TTS and image-gen
models do NOT accept `thinkingConfig` — omit the field entirely).
- **Optional: streaming TTS** — the TTS call can stream PCM audio
back as it generates so playback can start within ~3 seconds
rather than waiting for the full file. The server forwards the
PCM stream as a chunked HTTP response; the client wraps it in a
Media Source Extensions buffer for the `