# 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.
---
# YouTube → Event Flyer
## 1. Project
**YouTube → Event Flyer** is a one-screen tool that turns a YouTube
URL into a finished event promo kit. Paste the URL of the band's
livestream, the keynote talk, the conference session, the film
trailer, the charity-event recording — fill in the date, the venue,
the ticket link, the call-to-action — and twenty-five seconds later
you walk away with a 4K print-ready flyer, a 1:1 Instagram square,
a 16:9 LinkedIn / event-platform banner, a 9:16 story, and a
print-grade PDF, all visually consistent with the energy of the
source video.
The trick is **Nano Banana Pro video-to-image** — shipped 2026-05-28
inside `gemini-3-pro-image`. Pass it a public YouTube URL and the
model reads the video as a visual reference: the lighting of the
stage, the palette of the cinematography, the texture of the
backdrop, the dominant motion language. It uses that reference as
**style + subject inspiration** — never as a direct reproduction —
to generate matching promotional artwork at 4K with legible
in-image typography for the headline, the date, the venue, the
ticket call-to-action.
This is the kind of tool an indie band's tour manager needs at
2 a.m. the night before tickets go on sale: she has the livestream
URL of last month's show, the venue name, the date, the ticket
link, and zero budget for a designer. It is also the kind of tool a
conference programme lead uses to roll out 38 speaker promos in an
afternoon — paste each talk's YouTube URL, type the speaker's
name + room + time, and the kit lands on her desktop in a folder
ready to drop into the conference site and the social calendar.
Same shape of attention, different scale, different stakes.
The single demo that proves the magic: the visitor pastes a YouTube
URL of a livestream — a 7-piece soul band, warm stage lights, brass
section glinting, a velvet curtain backdrop. They type "Friday 22
August · The Curtain Room · 8 p.m. · tickets from £14". They tap
**Generate kit**. Twenty-five seconds later the screen fills with:
a 4K poster in warm amber and deep red, the band name set in a
condensed display serif at the top, "Friday 22 August" rendered
crisply across the lower third, "tickets from £14" sitting cleanly
above the venue line. A 1:1 Instagram tile with the headline
re-flowed for the square. A 16:9 banner with the headline pinned
left and the brass section silhouette filling the right. A 9:16
story crop. A print-ready PDF at 300 DPI with crop marks and a
3 mm bleed. Every artwork is *inspired by* the livestream's energy
— never a frame of it. The visitor downloads the zip and ships.
And in the harder cases — a 2-hour conference keynote where the
hero shot is one black-clad speaker against a blue-lit screen, a
charity gala recording with mixed lighting and a podium, a 90-second
film trailer with rapid cuts — the model still reads the video's
**aesthetic centre** (palette, lighting register, compositional
language) and produces a coherent kit. The user reviews, swaps an
asset they don't like for a regenerate, and exports.
**Tagline:** _Paste a YouTube URL. Out comes a poster, a square, a
banner, a story, and a print-ready PDF — all on-brand with the
video, in twenty-five seconds._
## 2. Target audience
- Independent musicians and tour managers promoting livestream
recordings, single drops, gigs, residencies, and tours — the
YouTube URL is often the only piece of brand truth they have
- Conference programme leads rolling out per-speaker promos —
paste the talk's URL, type the room and time, ship
- Film festival coordinators turning trailer URLs into 4K screening
posters, Instagram squares, and printed lobby cards
- Charity event teams promoting a gala, a fundraiser, a panel
discussion using last year's recording as the visual reference
- Workshop and class facilitators promoting a live cooking class, a
yoga session, a pottery workshop streamed last week to the same
audience
- Product launch teams who have a 90-second hero video and need
matching event collateral for the launch night
- Religious and community organisations promoting services,
cultural-night recordings, talks, concerts — many of these
organisations have YouTube as their primary media channel
- Indie podcasters with a video-podcast YouTube channel hosting a
live-show recording, who need posters and squares to promote the
upcoming live taping
- Theatre companies promoting upcoming productions using last
season's recorded performance as the visual reference
- Wedding bands, DJs, and event-entertainment companies whose
YouTube reel is their primary portfolio
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section
ordering — they are the reasons users pick this tool.
- **One URL is the brief** — the visitor pastes a public YouTube
URL and the model uses the video as a **style + subject
reference**. They do not upload reference imagery, they do not
write a creative brief, they do not pick a colour palette by
hand. The video is the brief.
- **Legible in-image typography at 4K** — Nano Banana Pro renders
real type, not pillowy AI-glyph hallucinations. The headline, the
date, the venue, the ticket call-to-action are crisp and
letter-spaced correctly. This is the post-I/O 2026 capability
that makes a real flyer possible inside a single generative call.
- **A full kit in one operation** — not just one image. A 4K
printable flyer, a 1:1 Instagram square, a 16:9 LinkedIn / event-
platform banner, a 9:16 story, and a print-grade PDF, all in one
pass. The visitor downloads the zip and ships.
- **Inspired by, never a reproduction** — every output is *inspired
by* the video's aesthetic. The model does not reproduce frames,
faces, logos, or brand marks visible in the video. The result is
promotional artwork that *feels like* the video; it is not an
extracted still.
- **Twenty-five seconds, end to end** — paste, type four event
fields, tap Generate. Twenty-five seconds later the kit is on
the screen. The visitor came in with no design tooling open;
they leave with files on their desktop.
- **Per-asset regenerate** — if one tile in the kit doesn't land,
tap **Regenerate this one** and the model rolls it again with
the same brief. The other five stay put.
- **Honest about likeness** — the tool **never** claims a likeness
licence. The artwork is *inspired by* the source video. If the
video has a recognisable performer in frame, the model is
prompted to generate **stylistic equivalents** — a silhouette, a
texture, a colour register — not a likeness. The capabilities
panel says this in plain English.
- **Works for any event tied to a video** — concerts, conferences,
film screenings, product launches, charity events, religious
services, theatre productions, workshops. The brief shape is
always the same: video URL + four event fields.
- **Print-grade PDF in the kit** — CMYK PDF at 300 DPI with a
3 mm bleed and crop marks. The printer can take the file straight
to plate.
## 4. Features to build
- One-screen paste-and-go input — large URL field, four event
fields (headline, date / time, venue, call-to-action), a
**Generate kit** button. That's the whole landing surface.
- **URL validator** — accepts `youtube.com/watch?v=`, `youtu.be/`,
`youtube.com/live/`, `youtube.com/shorts/`. Rejects private and
unlisted URLs with a specific error. Confirms the video is
public via the YouTube Data API (optional, see 4b).
- **Style sniffer (preview pass)** — before the full kit
generation, a fast pass extracts the video's aesthetic centre as
a `VideoBrief` (see schema in 4b). The visitor sees the
predicted palette, register, mood, dominant motion language —
with a one-tap "looks right / nudge warmer / nudge cooler" set
of adjustments — before committing to the 25-second full pass.
- **Kit generation** — single Nano Banana Pro call per asset, with
the YouTube URL passed as a `videoMetadata` part and the
`VideoBrief` + per-asset rendering instructions in the prompt.
Five assets per kit by default.
- **Per-asset regenerate** — re-roll one tile while keeping the
other four locked.
- **Per-asset edit** — open one tile in a side panel; edit
headline / date / venue / call-to-action; re-render only that
tile without re-sniffing the video.
- **Aspect ratio coverage** — 4:3 print poster (default A2 portrait
at 4K — 3937 × 5906 px), 1:1 (3000 × 3000), 16:9 (3840 × 2160),
9:16 (2160 × 3840). Each has its own per-asset prompt with
layout guidance for that ratio.
- **Print-grade PDF** — server-side composes the 4K PNG into a
300 DPI CMYK PDF with 3 mm bleed, crop marks, and the headline
set in a real font (not the model's rendered glyphs). The
rendered headline overlays the model's headline at the same
position; the model's rendered headline is hidden in the PDF
layer. This gives the printer crisp vector type while preserving
the photographic background.
- **Brand kit overlay (optional, second step)** — if the visitor
has a logo PNG and a brand colour, the second pass overlays the
logo at a configurable corner and threads the brand colour into
the palette via a follow-up Nano Banana Pro call (a maximum of
one brand-reference image plus the video — well under the 14-
reference cap).
- **Localisation** — date / time renders in the visitor's locale
(DD MMM YYYY in Europe, MMM DD YYYY in the US, with a toggle).
The headline is the user's own text; the model is instructed
not to translate it.
- **Download as zip** — one tap → zip containing the 5 assets +
the PDF + a `brief.json` with the `VideoBrief` and the user's
inputs, for reproducibility.
- **Share preview link** — magic-link URL that opens a read-only
view of the kit. Useful for sending to a bandmate or a programme
lead for approval before download.
- **Recent kits** — last 20 generated kits cached for the signed-in
user, with regenerate-from-history.
- **No likeness claim banner** — every generated asset carries a
small footer note in the export metadata (not on the visible
artwork): *"Inspired by the source video. Not a reproduction.
Performer likenesses are stylistic equivalents, not endorsed
or licenced."* The capabilities panel restates this prominently.
- **Public video only** — private, unlisted, and members-only URLs
are rejected. The error explains the rule.
## 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)
- **Nano Banana Pro video-to-image** (`gemini-3-pro-image`, shipped
2026-05-28) — the hero capability. Accepts a YouTube URL as a
`videoMetadata` / `fileData` part alongside the text prompt and
emits a 4K image with **legible in-image text**. This is what
makes the whole template possible: before 2026-05-28 the only
path was extracting a frame manually and using it as a reference
image, which is both ugly and licence-fraught. With video-to-
image the model reads the **aesthetic** across the timeline (not
one frame) and renders fresh artwork *inspired by* it.
*Note: Google shipped YouTube-URL ingestion with Nano Banana Pro
on 2026-05-28 but did not publish the exact request payload
schema — the `videoMetadata` / `fileData` part names and field
shape used in this spec are the working interpretation; verify
the SDK call structure against the live `gemini-3-pro-image`
reference before shipping.* **Guardrail: the YouTube URL must
be a public, fetchable URL — the model does not have
credentials, so private / unlisted / age-restricted URLs fail
at the model layer with a generic error. The server pre-
validates so the visitor sees a precise error.**
- **Style sniff** (`gemini-3.5-flash`, multimodal video input with
the same YouTube URL via `videoMetadata`) — returns a
`VideoBrief` JSON that names the aesthetic centre: palette
(3-5 hex colours), lighting register (warm / cool / neutral
/ high-contrast / low-contrast), mood (energetic / contemplative
/ celebratory / reverent / urgent), motion language (still /
smooth / kinetic / fragmented), dominant texture (velvet,
concrete, neon, paper, screen, fabric, foliage, glass, brass),
subject signifier (silhouette of a brass section, a single
podium, a panel of speakers, a film projector). This brief is
shown to the visitor for approval before the expensive Pro Image
generation; the same brief is then injected into the Pro Image
prompts. **Guardrail: the sniff explicitly emits stylistic
equivalents only — it never names a real performer, never
identifies a logo, never returns a face description usable to
recreate a likeness.**
- **Headline copy polish** (`gemini-3.5-flash`, thinkingLevel
`low`) — takes the visitor's raw event fields and emits a
per-asset headline variant tuned to that asset's character
count and aspect ratio. The 4:3 print poster can carry a long
headline; the 9:16 story needs it punched into a short pop.
This is the only place the model touches the user's words; it
never invents event details.
- **Structured output / JSON Schema** — the style-sniff call
returns `VideoBrief`. The kit-generation orchestrator validates
every per-asset prompt against `AssetSpec`. The kit-output
metadata is `EventKitManifest`. Schemas are seeded verbatim in
the system instruction and as `responseSchema` (except on the
Pro Image generation calls, which return image bytes — there
the prompt enforces structure via instruction).
- **Thinking levels** — `medium` on the style sniff (it reads the
video and reasons about aesthetic centre across a timeline);
`low` on the headline polish (a deterministic-feel touch-up,
not a reasoning task); the image-generation calls do **not**
accept `thinkingConfig` — omit the field entirely.
- **Public URL validation** (optional, YouTube Data API v3) —
before calling Gemini, the server can call YouTube Data API to
confirm the video is public and not age-restricted. If the
visitor doesn't supply a YT Data API key, the server falls back
to a HEAD request on the oEmbed endpoint
(`https://www.youtube.com/oembed?url=...`) which returns 200 for
public and 401 for private. The Gemini call also fails for
non-public URLs, but pre-validation gives a faster, kinder
error.
### Backend services
- **Auth — Optional (recommended).** Firebase Auth with Google
sign-in is auto-provisioned by AI Studio Build. Anonymous
sessions are supported for the first kit; subsequent kits
require sign-in (rate-limit reason). Apple sign-in is optional
and requires an Apple Developer account. **Post-I/O 2026:
Workspace integration is available without OAuth handshake**,
so a signed-in Workspace user can save kits directly to Drive
in a single click — this surfaces as a "Save kit to Drive"
button in the kit-ready view.
- **Database — Required.** Firestore for `users`, `kits`,
`regenerate_history`, `share_links`. Each kit document holds the
inputs (URL + event fields), the `VideoBrief`, the
`EventKitManifest`, and signed-URL references to the generated
asset blobs.
- **File storage — Required.** Firebase Storage for the generated
4K PNGs, the PDF, and the zip. **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 the
first generation runs. Pre-signed URLs only; assets expire after
30 days unless the user is signed in and pins the kit.
- **Server functions — Required.** Cloud Run handles:
(1) URL validation, (2) the style-sniff Gemini call,
(3) parallel per-asset Pro Image calls with retry, (4) PDF
composition with the real font on top of the photographic
background, (5) zip packaging, (6) signed-URL minting. **Free
2-app Cloud Run deploy from AI Studio Build covers this app
out of the box** (post-I/O 2026 addition).
- **Email — Optional.** Magic-link share emails for "send this
kit to my bandmate for approval". Sender domain must be
authorised in Firebase Auth.
- **Payments — Not needed for v1.** Free tier is 3 kits per day
per signed-in user. A future "Pro" tier (unlimited kits,
custom font upload, Adobe InDesign IDML export) might charge a
small subscription via Stripe; not built in v1.
- **External APIs:** Gemini API for all intelligence; optional
YouTube Data API v3 for pre-validation; optional Google Fonts
API (server-side resolution of the chosen display font for the
PDF overlay).
**Environment variables:** every secret (Gemini API key, Firebase
service-account JSON, optional YouTube Data API key, optional
Stripe key if Pro 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 kit' inside the UI · explicit opt-in for any analytics · the
visitor's event details, URLs, and generated assets 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 kit is per-kit and
revocable · public URLs only.
**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. **Use the post-I/O 2026 IDs**: `gemini-3.5-flash` for
text + multimodal-video sniffing and headline polish;
`gemini-3-pro-image` for the 4K kit generation with legible
in-image text. **Do NOT use** `gemini-3.5-flash`, `gemini-3.5-flash`,
`gemini-3.1-flash-image`, or `gemini-3.5-flash` — those are pre-I/O
strings and now resolve to deprecated endpoints.
2. **Pin `thinkingLevel` explicitly** per call. See the matrix.
On image-generation and any TTS calls, **omit `thinkingConfig`
entirely** — the field is not supported on those models.
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 (the YouTube URL via
`videoMetadata`, the event fields as text). 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. `responseSchema` and
`google_search` grounding **cannot be combined** in one call —
this template does not need grounding on any call, so the
conflict does not arise. If a future v2 wants to ground (e.g.
resolve a venue's address), instruct the model to emit JSON in
the text body and parse server-side; read citation URLs from
`response.groundingMetadata.groundingChunks[].web.uri`.
6. **State negative constraints explicitly** — they are listed
below. They are NOT "be careful" suggestions; they are hard
rules the model must follow.
7. **Pass video via `videoMetadata` part**, not as a URL embedded
in the text. The Gemini SDK accepts a `fileData` part with the
YouTube URL as `fileUri` and `mimeType: "video/youtube"`. This
is the only correct way to invoke video-to-image and video-
style-sniff today. Do not paste the URL into the text prompt
and hope the model fetches it.
8. **Long-context guardrail** — the style-sniff payload (system
instruction + brief context + one YouTube reference) is
well under 50k tokens. There is no chunking concern. If a
future v2 wants to sniff a multi-hour livestream, clamp the
sniff window to the first 5 minutes + a sampled middle minute
+ the final 2 minutes via `videoMetadata.start_offset` and
`end_offset` — never raw-dump the whole timeline.
9. **YouTube URL pre-flight + `.mp4` upload fallback.** Before
passing a YouTube URL to `gemini-3-pro-image` via
`videoMetadata`, server-side HEAD-check the page to verify
(a) not 404, (b) not private, (c) not geo-restricted from the
server region, (d) embedding not disabled, (e) not age-gated.
If pre-flight fails, return the elegant fallback modal: "This
video can't be read directly — upload the `.mp4` (or paste a
different URL)." Then accept a file upload, route it through
the Gemini Developer API Files API, and pass the resulting
`files/*` resource name (e.g. `files/abc123xyz`) via
`fileData: { fileUri, mimeType: "video/mp4" }` in place of the
YouTube URL. The downstream prompts are otherwise identical.
10. **Files API uses `files/*` resource names, not `gs://` URIs.**
The AI Studio Build runtime uses the Gemini Developer API
(`@google/genai` SDK). Files API `upload` returns a resource
name of the form `files/abc123xyz`, passed via `fileData:
{ fileUri, mimeType }`. `gs://` URIs belong to Vertex AI /
Cloud Storage — a different surface, not accepted here.
11. **Strip unsupported Zod modifiers before passing to
`responseSchema`** — Gemini's OpenAPI subset rejects `.regex()`
/ `pattern` (the `HexColor` regex below is a known offender),
fixed-length `z.tuple()`, and other custom validators. Use a
sanitizer that flattens tuples to arrays and removes regex
patterns before serializing. Validate hex format in
middleware AFTER parsing via `/^#[0-9A-Fa-f]{6}$/.test(value)`.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| URL → `VideoBrief` (style sniff) | `gemini-3.5-flash` | medium | (none); video via `videoMetadata` part |
| Event fields → per-asset headline variants | `gemini-3.5-flash` | low | (none) |
| 4:3 print poster (4K) | `gemini-3-pro-image` | n/a | (none); video URL passed via `videoMetadata` |
| 1:1 Instagram square (3000 × 3000) | `gemini-3-pro-image` | n/a | (none); video URL passed via `videoMetadata` |
| 16:9 banner (3840 × 2160) | `gemini-3-pro-image` | n/a | (none); video URL passed via `videoMetadata` |
| 9:16 story (2160 × 3840) | `gemini-3-pro-image` | n/a | (none); video URL passed via `videoMetadata` |
| Brand overlay (optional second pass) | `gemini-3-pro-image` | n/a | (none); video URL + 1 brand-reference image |
| Hero / empty-state illustration | `gemini-3.1-flash-image` (Nano Banana 2) | n/a | (none) |
| Capabilities panel narration (accessibility TTS) | `gemini-3.1-flash-tts-preview` | n/a | n/a |
*Note for builders:* on image-generation and TTS 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. None of the calls in
this template need grounded search, so the
`responseSchema` ↔ `google_search` conflict does not arise.
### Primary structured-output schemas (seed verbatim in the prompt)
```typescript
import { z } from "zod";
// NOTE: strip this .regex() before passing the schema to `responseSchema` —
// Gemini's OpenAPI subset rejects `pattern`. Validate hex format in middleware
// AFTER the model returns: /^#[0-9A-Fa-f]{6}$/.test(parsed.palette_primary)
const HexColor = z.string().regex(/^#[0-9A-Fa-f]{6}$/);
const LightingRegister = z.enum([
"warm",
"cool",
"neutral",
"high_contrast",
"low_contrast",
"mixed",
]);
const Mood = z.enum([
"energetic",
"contemplative",
"celebratory",
"reverent",
"urgent",
"intimate",
"playful",
"cinematic",
]);
const MotionLanguage = z.enum([
"still",
"smooth",
"kinetic",
"fragmented",
"swelling",
"rhythmic",
]);
const Texture = z.enum([
"velvet",
"concrete",
"neon",
"paper",
"screen",
"fabric",
"foliage",
"glass",
"brass",
"wood",
"stone",
"skin_no_likeness",
"other",
]);
const VideoBrief = z.object({
source_url: z.string().url(),
source_url_is_public: z.boolean(),
duration_observed_seconds: z.number().nullable(),
palette_primary: HexColor,
palette_secondary: HexColor,
palette_accent: HexColor,
palette_neutral_dark: HexColor,
palette_neutral_light: HexColor,
lighting_register: LightingRegister,
mood: Mood,
motion_language: MotionLanguage,
dominant_texture: Texture,
secondary_texture: Texture.nullable(),
subject_signifier_one_line: z.string(), // "silhouette of a 7-piece brass band on a low stage"
composition_language: z.string(), // "wide static frame with brass section centred, warm spill from stage right"
rejected_likeness_note: z.string(), // "no recognisable performer faces or logos surfaced"
text_overlay_safe_zone: z.enum([
"top_band",
"bottom_band",
"left_third",
"right_third",
"centred",
"no_safe_zone_recommend_solid_backdrop_in_kit",
]),
sniff_confidence: z.number().min(0).max(1),
flagged_for_user_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
});
const EventInputs = z.object({
headline: z.string().min(1).max(120),
date_iso: z.string(), // canonical ISO; UI renders per locale
venue: z.string().min(1).max(120),
call_to_action: z.string().min(1).max(80),
brand_color_hex: HexColor.nullable(),
brand_logo_uri: z.string().nullable(), // Files API `files/*` resource name; optional
});
const AssetSpec = z.object({
asset_id: z.string(),
aspect_ratio: z.enum(["4:3", "1:1", "16:9", "9:16"]),
pixel_width: z.number(),
pixel_height: z.number(),
intent: z.enum([
"print_poster",
"instagram_square",
"banner_16_9",
"story_9_16",
]),
headline_variant: z.string(),
date_string_for_render: z.string(),
venue_string_for_render: z.string(),
cta_string_for_render: z.string(),
text_overlay_safe_zone: VideoBrief.shape.text_overlay_safe_zone,
});
const RenderedAsset = z.object({
asset_id: z.string(),
storage_uri: z.string(), // Firebase Storage path to the 4K PNG (signed URLs surface to clients)
pdf_storage_uri: z.string().nullable(), // present only for print_poster
width_px: z.number(),
height_px: z.number(),
generation_seconds: z.number(),
model_id_used: z.string(), // "gemini-3-pro-image"
generation_revision: z.number(), // 1 on first try, 2+ if regenerated
});
const EventKitManifest = z.object({
kit_id: z.string(),
created_at_iso: z.string(),
source_url: z.string().url(),
inputs: EventInputs,
video_brief: VideoBrief,
asset_specs: z.array(AssetSpec),
rendered_assets: z.array(RenderedAsset),
zip_storage_uri: z.string().nullable(),
share_link_storage_uri: z.string().nullable(),
not_a_reproduction_disclaimer: z.string(), // surfaced in export metadata
});
type VideoBrief = z.infer;
type EventInputs = z.infer;
type AssetSpec = z.infer;
type RenderedAsset = z.infer;
type EventKitManifest = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent picks `gemini-3.5-flash` for the image generation to save a
call — that ID is **deprecated** post-I/O 2026 and the call will
resolve to a missing endpoint. Pin `gemini-3-pro-image`
explicitly for every kit-generation call.
- Agent pastes the YouTube URL into the text prompt and hopes the
model fetches it — Gemini does not fetch URLs from inside the
text. The URL **must** be passed via the `videoMetadata` /
`fileData` part with `mimeType: "video/youtube"`. If the agent
pastes the URL into text, the model invents a video that doesn't
exist and hallucinates a `VideoBrief`. Validate server-side that
the request has the `videoMetadata` part.
- Private / unlisted / age-restricted URL — the Gemini call fails
with a generic error. Pre-validate via YouTube oEmbed (returns
401 for private) and emit a precise error: "This video is private
or unlisted — only public YouTube URLs work."
- Likeness reproduction — without explicit constraint the model
may try to recreate a recognisable performer's face on the
poster. Every Pro Image prompt must include the constraint
*"generate stylistic equivalents only — silhouettes, textures,
and lighting that evoke the source video; do NOT reproduce
faces, logos, brand marks, or copyrighted graphics visible in
the source"*. The `rejected_likeness_note` field in the
`VideoBrief` confirms the sniff also obeyed this.
- Legible text isn't actually legible — even Nano Banana Pro can
drop a glyph if the prompt asks for too much copy at too small
a size. Constrain headline to ≤ 30 chars on 9:16, ≤ 45 chars on
1:1 and 16:9, ≤ 80 chars on 4:3 print. The headline-polish call
produces per-asset variants that respect these caps; the kit
generation refuses to send a headline that exceeds the cap.
- PDF overlay drift — the model's rendered headline and the PDF
font overlay don't sit in exactly the same spot. The fix is to
ask the model to render the headline in a *safe zone* (top band
/ bottom band / left third / right third) determined by the
sniff. The PDF composer then overlays the real font in that
same safe zone and **hides** the model's rendered headline
layer in the print PDF (the photographic background underneath
remains intact).
- Date / time formatting drift — the model renders "8/22/2026" on
a European-targeted flyer or "22/8/2026" on a US-targeted one.
The fix is to compute `date_string_for_render` server-side in
the user's locale and pass it verbatim into the prompt with the
instruction *"render the date exactly as: ; do not
reformat"*.
- 9:16 story crops the headline — the model centres the
composition at 1:1 and the 9:16 crop drops the top of the
headline. Per-asset prompts include layout guidance ("for 9:16
vertical, place the headline in the lower third, keep the upper
half for the subject signifier").
- Brand colour clashes with the video palette — if the user
supplies a brand colour that is the complement of the video's
primary, the model may render a poster that looks washed.
Surface a warning in the UI ("your brand colour sits opposite
the video's palette — proceed, or pick a complementary
highlight?") and offer to thread the brand colour as a single
accent stripe instead of a base.
- Multilingual headlines — the user may type the headline in
Arabic, Tamil, Korean, or Russian. Pro Image renders Latin
glyphs better than CJK or Arabic at small sizes. The headline-
polish call's system instruction must explicitly preserve the
user's script verbatim (no translation) and flag the asset for
manual review with a low confidence if the script is CJK,
Arabic, Hebrew, Devanagari, Tamil, Thai, or Khmer — in those
cases prefer the PDF overlay (real font) and instruct the model
to render only the subject signifier, leaving a clean safe zone
for the headline.
- Style sniff hallucinates a performer name or band name — the
sniff system instruction must **forbid** naming any
recognisable performer, band, or brand visible in the video.
The `subject_signifier_one_line` describes what is seen
generically ("a seven-piece brass band on a low stage"), never
by name.
- Generation times out — Pro Image at 4K can take 8-12 seconds
per asset. Run the four kit assets in parallel from the server.
The total kit time should be ~25 seconds including the sniff,
not 50 seconds serial.
### Negative constraints (hard rules)
- Do NOT claim a likeness licence. Every generated asset is
*inspired by* the source video. The capabilities panel says
this in plain English and the export metadata carries the
not-a-reproduction note.
- Do NOT reproduce a recognisable performer's face, a logo, a
brand mark, a wordmark, or any copyrighted graphic visible in
the source video. Generate stylistic equivalents — silhouette,
texture, palette, lighting register.
- Do NOT accept private, unlisted, age-restricted, or members-
only YouTube URLs. Pre-validate server-side; emit a precise
error.
- Do NOT paste the YouTube URL into the text prompt of any
Gemini call. Pass it via the `videoMetadata` / `fileData` part
with `mimeType: "video/youtube"` so the model actually reads
the video.
- Do NOT use `gemini-3.5-flash`, `gemini-3.5-flash`, `gemini-3.5-flash-
image`, or `gemini-3.5-flash`. They are pre-I/O 2026 strings and
now resolve to deprecated endpoints. Use the post-I/O IDs in
the matrix above.
- Do NOT translate the user's headline. Render it verbatim, in
the script the user typed.
- Do NOT reformat the date string. The server computes the
locale-correct string and the prompt passes it verbatim.
- Do NOT invent event details. If the user did not supply a
call-to-action, leave that field empty in the asset — do not
hallucinate "tickets from £X".
- Do NOT use the visitor's URL, event inputs, or generated kit 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
panel says this in plain English.
- Do NOT auto-publish kits. Sharing is explicit, per-kit, and
revocable.
- Do NOT label the generated kit "designed by AI" on the artwork
itself. The intelligence is in the experience, not a badge on
the poster.
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini
API call the built app makes.
### Call: URL → `VideoBrief` (style sniff)
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none); video via `videoMetadata` part
```
You are sniffing the aesthetic centre of a public YouTube video
to produce a VideoBrief JSON object that another model will use
as a style reference for promotional artwork.
You receive the video as a `videoMetadata` / `fileData` part with
`mimeType: "video/youtube"` and `fileUri` set to the user's
YouTube URL. You also receive a short text header naming the
event type ("concert", "conference talk", "film screening",
"product launch", "charity event", "religious service", "theatre
production", "workshop") so you can disambiguate ambiguous
aesthetics.
Read the video across its timeline. Sample lighting, palette,
mood, motion language, dominant texture, and the subject
signifier. Look for the most distinctive 1-3 visual anchors that
define this video's energy — not the average frame, not the
title card, not the end card. The anchors are what a designer
would point at and say "that's what this gig feels like".
Hard rules:
- Do NOT name any recognisable performer, band, speaker, brand,
or organisation visible in the video. The
subject_signifier_one_line is generic: "a seven-piece brass
band on a low stage", "a single speaker at a podium in front
of a blue-lit screen", "a wide stage with a string quartet
and a soloist", "a panel of four speakers around a low table".
- Do NOT describe faces in a way that could be used to recreate
a likeness. Describe silhouette, pose, costume colour, but not
facial features.
- Do NOT identify logos, brand marks, sponsor banners, or
wordmarks visible in the video. If a sponsor banner is
prominent, note "a low-contrast banner sits behind the stage —
treat as neutral backdrop, do not reproduce" in
rejected_likeness_note.
- The palette must be 5 hex colours that together summarise the
video's colour register: a primary, a secondary, an accent, a
dark neutral, a light neutral. Do NOT return more or fewer.
- lighting_register, mood, motion_language, dominant_texture are
each constrained to the enum values. If multiple apply, pick
the most dominant; if the video is genuinely split, set
lighting_register to "mixed" and explain in
flagged_for_user_review.
- text_overlay_safe_zone names the area of the frame that is
visually quiet enough to drop a headline into. If the entire
frame is busy, set "no_safe_zone_recommend_solid_backdrop_in_kit"
so the kit generator knows to render a coloured backdrop
behind the headline rather than relying on the video imagery.
- sniff_confidence reflects how distinctive the video's aesthetic
is. A flat conference podium against a beige wall is low
confidence (0.4); a heavily-lit gig with a strong colour
language is high confidence (0.9).
- rejected_likeness_note is REQUIRED and must explicitly state
"no recognisable performer faces, logos, or brand marks were
surfaced — stylistic equivalents only will follow" so the
downstream model receives the constraint in its context.
Output ONLY the VideoBrief JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Event fields → per-asset headline variants
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You polish event fields into per-asset headline variants for a
promotional kit. You receive the user's raw headline, date_iso,
venue, call_to_action, and the list of AssetSpec aspect ratios
to fill. You emit one headline_variant per asset, respecting the
per-aspect character cap.
Caps:
- 4:3 print poster: ≤ 80 chars
- 1:1 instagram square: ≤ 45 chars
- 16:9 banner: ≤ 45 chars
- 9:16 story: ≤ 30 chars
Hard rules:
- The headline is the user's words. You may shorten by removing
filler words ("The", "A", "with") to fit the cap, but you do
NOT translate, do NOT add words, do NOT invent details.
- If the user's headline already fits the cap, return it
verbatim.
- If the user's headline is in a non-Latin script (Arabic,
Hebrew, CJK, Devanagari, Tamil, Thai, Khmer), do NOT
transliterate; return the script verbatim and flag the asset
for manual review by setting a `notes` field if you support
one — at minimum, do not silently drop characters.
- Preserve numerals, dates, and proper nouns exactly.
- Preserve punctuation that carries meaning (em-dashes, colons,
ampersands). You may drop oxford commas to save chars on the
9:16 only.
Output a JSON array of objects keyed by asset_id with one
headline_variant per asset. No commentary.
```
---
### Call: 4:3 print poster (4K)
Model: `gemini-3-pro-image` · n/a · Tools: (none); video URL passed via `videoMetadata`
```
You generate a single 4K print-ready poster image at 3937 × 5906
pixels (A2 at 300 DPI, portrait). The user has provided a
public YouTube URL as a video reference; the VideoBrief above
describes the aesthetic centre. Use the video as a STYLE +
SUBJECT REFERENCE only — never reproduce frames, faces, logos,
brand marks, sponsor banners, or copyrighted graphics from it.
Inputs you receive in context:
- The YouTube URL via `videoMetadata` (mimeType: "video/youtube").
- The full VideoBrief JSON.
- The per-asset AssetSpec for this 4:3 print poster, including
the locale-formatted date_string_for_render, the
venue_string_for_render, the cta_string_for_render, and the
headline_variant for 4:3.
- The text_overlay_safe_zone (top_band / bottom_band / left_third
/ right_third / centred / no_safe_zone_recommend_solid_backdrop).
Composition rules:
- Render the headline in legible 4K typography, ≤ 80 chars, in
the safe zone named by AssetSpec. Use a condensed display
serif or a high-impact sans depending on the brief's mood:
"energetic" / "celebratory" → high-impact sans;
"contemplative" / "reverent" → display serif;
"urgent" / "cinematic" → condensed display sans;
"intimate" / "playful" → warm humanist sans;
"cinematic" → wide display sans.
- Render the date_string_for_render below or beside the
headline depending on safe zone. Render it EXACTLY as
provided. Do NOT reformat. Do NOT translate month names.
- Render venue_string_for_render in a smaller weight.
- Render cta_string_for_render in the smallest weight, with
optional underline.
- The photographic background is *inspired by* the video — a
scene that evokes the same palette, lighting register, mood,
and motion. NEVER a frame from the video; ALWAYS a freshly
generated scene.
- Use the VideoBrief palette: palette_primary and
palette_secondary dominate; palette_accent is a single
highlight; palette_neutral_dark and palette_neutral_light
provide text contrast.
- Subject signifier is a STYLISTIC EQUIVALENT of what the brief
describes — silhouette of a brass section, abstracted form of
a string quartet, a low-lit podium with negative space.
Never a likeness of a real person.
- If text_overlay_safe_zone is
"no_safe_zone_recommend_solid_backdrop_in_kit", render the
poster with a solid backdrop band in palette_neutral_dark or
palette_neutral_light behind the headline, and the
photographic scene fills the remaining area.
- Leave a 3 mm bleed equivalent (47 px at 300 DPI on each side)
empty of critical content — the print PDF will add crop
marks in this zone.
Hard constraints (all are MUST):
- 3937 × 5906 px, 4:3 portrait at print scale.
- Legible in-image text — no glyph dropouts, no pillowy AI
hallucination glyphs.
- Inspired-by, not reproduction-of. The video is a reference
for palette, light, and mood — not for content.
- No recognisable performer faces, logos, brand marks, or
wordmarks.
- No additional copy beyond the headline, date, venue, and CTA
fields supplied. Do NOT invent ticket prices, sponsor names,
contact emails, or QR codes.
- No "AI" badge, no model watermark, no signature.
Output: the 4K PNG.
```
---
### Call: 1:1 Instagram square (3000 × 3000)
Model: `gemini-3-pro-image` · n/a · Tools: (none); video URL passed via `videoMetadata`
```
You generate a single 3000 × 3000 px square asset for Instagram.
Use the same VideoBrief, the same inspired-by rule, the same
no-likeness rule as the 4:3 print poster.
Composition rules:
- Headline ≤ 45 chars in the safe zone named by AssetSpec.
- Date below or beside headline.
- Venue smaller, CTA smallest.
- Subject signifier sits in the half of the frame opposite the
headline (text on top → subject in bottom; text on bottom →
subject on top; text on left → subject on right).
- Same palette and same mood as the 4:3 — the kit must feel
visually coherent across all assets.
- Critical content stays within the central 2400 × 2400 (Instagram
crops the edges on some viewports).
Hard constraints: same as 4:3 (legible, inspired-by, no
likeness, no invented details, no AI badge).
Output: the 3000 × 3000 PNG.
```
---
### Call: 16:9 banner (3840 × 2160)
Model: `gemini-3-pro-image` · n/a · Tools: (none); video URL passed via `videoMetadata`
```
You generate a single 3840 × 2160 px banner for event platforms
(Eventbrite, Luma, LinkedIn Events) and YouTube channel banners.
Composition rules:
- Headline ≤ 45 chars on the LEFT or RIGHT third of the frame,
whichever the safe zone names.
- Subject signifier fills the opposite half.
- Date and venue render below the headline in the same column.
- CTA is optional in this size — keep it small if surfaced.
- Same palette, mood, and inspired-by rules as siblings.
Hard constraints: same as 4:3 and 1:1.
Output: the 3840 × 2160 PNG.
```
---
### Call: 9:16 story (2160 × 3840)
Model: `gemini-3-pro-image` · n/a · Tools: (none); video URL passed via `videoMetadata`
```
You generate a single 2160 × 3840 px vertical asset for
Instagram / TikTok stories.
Composition rules:
- Headline ≤ 30 chars in the LOWER third (safe zone above the
swipe-up area).
- Date below the headline.
- Venue smaller, CTA smallest.
- Subject signifier fills the upper two thirds.
- Keep critical content above the bottom 250 px (story
navigation overlay).
- Same palette and mood as siblings.
Hard constraints: same as 4:3, 1:1, and 16:9.
Output: the 2160 × 3840 PNG.
```
---
### Call: Brand overlay (optional second pass)
Model: `gemini-3-pro-image` · n/a · Tools: (none); video URL + 1 brand-reference image
```
You receive one already-generated kit asset plus a single brand
reference: a logo PNG and an optional brand_color_hex. Your job
is to re-render the asset with the brand colour threaded as a
single accent stripe or thin border, and the logo placed in the
corner specified (top_left / top_right / bottom_left /
bottom_right).
Hard rules:
- Place the logo at the specified corner with a 5% margin from
the edge.
- Logo size is 8% of the asset's shorter edge — never larger.
- Brand colour appears as ONE accent stripe at most — never as
the dominant palette. The video's palette stays primary.
- Do NOT modify the headline, date, venue, or CTA copy.
- Do NOT add additional brand copy ("Proudly sponsored by",
etc.) unless the user supplied it.
Output: the re-rendered PNG at the asset's original dimensions.
```
---
### Call: Hero / empty-state illustration
Model: `gemini-3.1-flash-image` (Nano Banana 2) · n/a · Tools: (none)
```
You generate a single photographic-looking image for the
welcome / empty-state of the app. The scene depicts a quiet
desk-side moment at the edge of a creative production: a paper
flyer laid flat on a wooden desk, a phone beside it showing a
paused YouTube player, warm overhead lamp light, no people in
frame.
Prompt anchors that work well:
- "warm desk lamp on a wooden desk, an A2 printed event flyer
laid flat with a venue date and a band name visible at the
bottom (use generic placeholder text), a phone next to it
showing a YouTube player paused on a warm-lit stage shot,
no people, soft shadow, real paper texture"
- "a paste-board pinned with three event flyers in different
aspect ratios — a portrait poster, a square, a wide banner —
warm afternoon light through a studio window, no people, no
brand logos visible"
Hard rules:
- Photographic, not cartoon.
- No people in frame.
- No real brand marks, no real venue names — use plausible
generic placeholders.
- Warm light, slight imperfection, real paper texture.
- Aspect ratios: 3:2 for hero, 1:1 for empty states.
```
---
### Call: Capabilities panel narration (accessibility TTS)
Model: `gemini-3.1-flash-tts-preview` · n/a · Tools: n/a
```
Voice: clear, warm, unhurried. Pick the Gemini 3.1 Flash TTS
voice whose `languageCode` matches the user's locale.
Use case: accessibility narration of the capabilities panel for
visitors who use a screen reader or prefer audio. The narration
walks through what the app does, what models it uses, the
not-a-reproduction policy, and the public-URL-only rule.
Pre-process the text before sending to TTS:
- Source the text from the capabilities-panel copy in section
6c.
- At sentence boundaries, insert an ellipsis ("…") for a
natural pause. At paragraph boundaries, insert a blank line
plus an em-dash ("—"). Gemini 3.1 Flash TTS does NOT support
SSML `` tags — these textual cues are how pace
is conveyed.
- Skip code-like content (model IDs, env-var names) and
replace with spoken equivalents ("Gemini three point five
Flash", "your Gemini A P I key").
- Target rate: ~135 words per minute — slightly slower than
conversational pace for accessibility.
Style direction: prepend ONE short directive sentence to the
text input, exactly like: "Read clearly and warmly, like
explaining how a tool works to a curious friend. …".
There is no separate `style` API field on Gemini 3.1 Flash TTS;
the directive sentence inside the input is how style is
conveyed.
Phoneme overrides for brand names, venue names, etc. are NOT
exposed by Gemini 3.1 Flash TTS — no SSML `` tag.
Pronunciation comes from the chosen voice's native locale.
```
## 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.
- **The 2 a.m. tour-manager run.** A tour manager pastes the URL
of last month's livestream. The video is a 7-piece soul band in
a velvet-curtained club, warm amber stage lights, a brass
section glinting under the spotlights. She types "Friday 22
August · The Curtain Room · 8 p.m. · tickets from £14". The
sniff returns a palette anchored on amber `#C57B3A` and deep
red `#8B2A2A` with brass `#D4A24C` as the accent, mood
"celebratory", motion "rhythmic", subject signifier "silhouette
of a brass section on a low stage". Twenty-five seconds later
the kit is ready. She downloads the zip and ships to the venue
by 2:30.
- **38 speaker promos in an afternoon.** A conference programme
lead pastes each talk's YouTube URL, types speaker name + room
+ time + "Register at: …". The kit lands in 25 seconds. She
drops the 16:9 into the conference site and the 1:1 into the
social calendar. Repeat 38 times. The whole rollout that used
to take a design intern three days is done before 6 p.m.
- **Film festival lobby cards.** A festival coordinator pastes the
YouTube trailer URL of a screening film. The sniff reads a 90-
second trailer with rapid cuts — the dominant texture is
"screen" (the film's own grainy frame), lighting register
"low_contrast", mood "cinematic". The 4:3 print poster lands
with the headline in a condensed display sans, the film title
rendered crisply across the lower band, the screening date and
venue in the smaller weights. The lobby gets new cards every
Wednesday.
- **Charity gala promo.** A charity team pastes last year's gala
recording. The sniff reads warm stage lighting, a podium, a
string quartet — mood "celebratory" with a "reverent" register.
The kit comes back warm and dignified. The team adds the
charity logo at the bottom-right via the brand-overlay second
pass.
- **Indie podcast live-show promo.** A podcaster with a video-
podcast YouTube channel pastes the URL of last month's live
recording. The sniff reads a black-clad host on a low stage
with a single guest in front of a green-lit screen, mood
"intimate", texture "screen". The kit's 9:16 story sits
perfectly inside the host's existing visual language.
- **Community-cultural-night promo.** A community organisation
pastes the URL of last year's cultural night — a stage with
dancers in saffron and indigo costumes under low warm light.
The sniff reads palette indigo `#2A2A6B` and saffron `#D89738`,
mood "celebratory", motion "rhythmic", subject signifier
"abstract silhouettes of dancers in flowing costume". The
generated kit reads as a poster for a cultural night, not a
generic event flyer.
- **Workshop promo.** A pottery teacher pastes the YouTube link
of last week's recorded class. The sniff reads soft daylight, a
ceramic wheel, hands shaping clay (no faces visible), mood
"contemplative", texture "clay" (closest enum: "stone"). The
4:3 portrait lands with a centred ceramic-textured backdrop and
the workshop date in a humanist serif.
- **Theatre season opener.** A small theatre pastes last
season's recording of a Shakespeare production. The sniff
reads "cinematic" with a "low_contrast" register and a
"velvet" texture. The kit lands in deep reds and blacks, the
headline set in a condensed display serif, ready for both
print and the digital playbill.
- **Religious-service kit.** A house of worship pastes a recent
recording of a service. The sniff reads "reverent" with
"warm" lighting and "stone" texture (from the venue
architecture). The poster lands warm and dignified, ready for
the bulletin board.
- **Wedding-band reel turned event poster.** A wedding band
pastes their portfolio reel and types "Saturday 14 March · The
Conservatory · doors 7 p.m. · £25 / £18 concession". The kit
comes back in the band's own palette, ready to ship to the
venue's printer.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy
to fit the voice, but keep the structural intent.
1. **Landing / paste page.** A single, calm screen. Above the
fold: a one-line tagline ("Paste a YouTube URL. Get back a
poster, a square, a banner, a story, and a print-ready PDF.")
and the URL input field. Below: the four event fields
(headline, date / time, venue, call-to-action). One big
primary button: **Generate kit**. Below that, a small "see an
example" link that loads the demo lineage in section 8a. A
short reassurance line: "Public videos only. Inspired by, not
reproduced." No hero with floating orbs. No row of stat cards.
2. **Sniff preview.** After the URL is validated and the style
sniff has run (3-4 seconds), a small modal/inline panel
shows: the palette as 5 colour chips, the mood + lighting
register + motion language as pills, the
subject_signifier_one_line as a sentence, and a
text_overlay_safe_zone hint. Three buttons: **Looks right —
generate kit** (default), **Nudge warmer**, **Nudge cooler**.
Nudge re-runs the sniff with a hint.
3. **Kit generation in flight.** Five tiles arranged in a 2 × 3
grid (4:3 print, 1:1, 16:9, 9:16, and the PDF). Each tile
shows a streaming generation state with the per-asset prompt
summary visible ("4:3 print poster · amber + deep red ·
condensed display serif · headline in bottom band"). When an
asset arrives, the tile fades in to the 4K result. Each tile
has a **Regenerate this one** button below.
4. **Kit ready.** The 5 tiles + PDF preview, each tappable to
open in a side panel for inspection at native resolution.
Primary CTA: **Download zip**. Secondary: **Save kit to
Drive** (visible only for signed-in Workspace users — uses
the post-I/O 2026 Workspace-without-OAuth integration).
Tertiary: **Share preview link** (magic-link URL for approval).
5. **Per-asset edit panel.** Open one tile → edit the
headline / date / venue / CTA for just that asset → tap
**Re-render** → only that asset rolls again. Useful when one
tile has a typo or a venue-specific tweak.
6. **Recent kits.** A grid of the user's last 20 kits, each as a
thumbnail of the 1:1 tile. Tap to open, regenerate, or share.
Cached for 30 days; pinned kits cached indefinitely.
7. **Brand kit (optional).** A small panel that lets the user
upload a logo PNG and pick a brand colour. If supplied, the
kit-ready view shows a **Apply brand to kit** button that
runs the brand-overlay second pass.
8. **Capabilities info button (header `(i)` icon).** Persistent.
Opens a modal — see 6c.
9. **Settings & privacy.** Locale, date-format preference (DMY /
MDY / ISO), notification preferences (none by default — this
is a one-shot tool, not a long-running notebook), export
defaults (zip / individual files), "Delete this kit forever"
and "Delete this account" controls with a 60-second cool-off.
Privacy panel restates the not-trained-on policy in plain
English.
10. **Footer.** "Inspired by the video. Built for the night before
tickets go on sale." Privacy: "Your URLs and event details
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: "Paste a URL. Get the kit."
- Subhead: "A 4K poster, an Instagram square, a banner, a story,
and a print-ready PDF — all visually consistent with the
video's energy. Twenty-five seconds, end to end."
- One paragraph (≤ 60 words) explaining who this is for and what
makes it different from a generic flyer tool: the YouTube URL
*is* the brief, Nano Banana Pro reads the video as a style +
subject reference, every asset is freshly generated and
inspired by the video — never extracted from it.
- Visual: a small annotated illustration of a poster being
generated from a YouTube thumbnail, with the palette chips
flowing across — not a generic stock-photo hero.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample URL".
- A live demo input pre-loaded with the soul-band concert URL
from the seed content in section 8a.
- 1-2 sentences pointing at *the specific page elements* where
the Gemini magic happens (the sniff preview after 4 seconds,
the legible in-image typography at 4K, the per-asset
regenerate).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Wire your Gemini API key and Firebase project via the env-
var list in the capabilities panel."
- "Adjust the per-asset prompts in `/server/prompts/` to fit
your house style or your typical event types."
- "Tune the headline-cap caps in `/server/spec/asset-caps.ts`
if your headlines run longer or shorter than the defaults."
- 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):**
- **Nano Banana Pro (`gemini-3-pro-image`)** — the post-I/O 2026
hero image model, shipped 2026-05-28. Reads your YouTube URL
as a **style + subject reference**, renders 4K artwork with
**legible in-image text** for headlines, dates, venues, and
call-to-action. Each kit asset is one Pro Image call. This is
what makes the whole template possible — before 2026-05-28
legible typography at 4K from a single generative call was
not reliable.
- **Gemini 3.5 Flash (multimodal video)** — sniffs the
aesthetic centre of your YouTube URL: palette, lighting,
mood, motion language, dominant texture, subject signifier.
Returns a typed `VideoBrief` that the image generator uses as
context. Also handles the headline-polish call for per-asset
character caps.
- **Gemini 3.5 Flash (structured output)** — every sniff
returns a typed `VideoBrief`; every kit is a typed
`EventKitManifest`. The schemas live in the repo.
- **Nano Banana 2 (`gemini-3.1-flash-image`)** — generates the
welcome and empty-state imagery (the desk-and-flyer scene,
the paste-board mock-up). Faster and cheaper than Pro; not
used for kit output.
- **Gemini 3.1 Flash TTS (`gemini-3.1-flash-tts-preview`)** —
narrates this capabilities panel for accessibility playback.
- **Firebase Auth** — Google sign-in (optional for first kit,
required from kit 2 onwards for rate-limit reasons).
- **Firestore** — stores your kits, syncs across devices, with
per-user access rules.
- **Firebase Storage** — keeps your generated 4K PNGs, the PDF,
and the zip. 30-day default expiry; signed-in users can pin
kits to keep them indefinitely.
- **Cloud Run** — server-side orchestration (URL validation,
parallel Pro Image calls, PDF composition, zip packaging,
signed-URL minting). **Free 2-app Cloud Run deploy from AI
Studio Build** covers this app out of the box (post-I/O 2026
addition).
- **Workspace integration without OAuth** — signed-in Workspace
users get a one-click "Save kit to Drive" button (post-I/O
2026 addition).
- **YouTube Data API v3** (optional) — pre-validates the
visitor's URL is public and not age-restricted. If you don't
supply a YouTube Data API key, the app falls back to the
oEmbed endpoint for validation.
**Cost note** — see the detailed breakdown in 6d. A single kit
generation (sniff + 4 Pro Image assets + headline polish + PDF
composition) costs about $0.24 of Gemini API spend, plus
negligible storage. Recent-kits caching keeps re-views free.
**Privacy note** — your URLs, event details, and generated kits
are private to you and any user you explicitly share a kit with
via the magic-link feature. 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.
**Likeness + IP note (read carefully):**
- **Every output is *inspired by* the source video. Never a
reproduction.** The video is a style reference (palette,
lighting, mood, motion) and a subject signifier reference
(a brass section, a podium, a panel of speakers) — never a
source of frames, faces, logos, or brand marks.
- **The tool does NOT claim a likeness licence.** If the source
video features a recognisable performer, the generated
artwork uses stylistic equivalents (silhouettes, costume-
colour blocks, lighting registers) — never a face that could
be construed as the performer's.
- **Public videos only.** Private, unlisted, age-restricted, and
members-only URLs are rejected. We never bypass YouTube's
access controls.
- **Your event details are yours.** You provide the headline,
date, venue, and call-to-action. The tool renders them as you
typed them; it does not invent ticket prices, sponsor names,
or contact emails.
**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)
- `FIREBASE_STORAGE_BUCKET` — your Firebase Storage bucket name
- `YOUTUBE_DATA_API_KEY` — optional; if omitted, oEmbed fallback
is used
**Documentation links:**
- AI Studio Build docs (free 2-app Cloud Run deploy, Workspace
integration without OAuth, post-I/O 2026 surface upgrades)
- Gemini API multimodal video docs (Nano Banana Pro video-to-
image, `videoMetadata` part shape)
- Gemini 3.5 Flash docs (multimodal video input, structured
output)
- Firebase Auth, Firestore, Firebase Storage docs
- YouTube Data API v3 docs
**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: "What powers this app".
- 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)
- **Style sniff (Gemini 3.5 Flash, medium thinking, 1 YouTube
reference)** — a 5-minute video sampled across the timeline
averages ~25,000 input tokens (the video tokens) and ~700
output tokens. At Gemini 3.5 Flash pricing ($1.50/M input,
$9/M output) that is ~$0.044 per sniff. Cached on the kit, so
repeated views of the same kit do not re-sniff.
- **Headline polish (Gemini 3.5 Flash, low thinking)** — small
input (event fields + asset specs), ~600 input tokens and
~250 output tokens → ~$0.0033 per kit.
- **Nano Banana Pro 4K poster (gemini-3-pro-image)** — one 4K
image generation with a YouTube URL reference and the
VideoBrief in context. Image-gen calls are priced per image,
not per token; assume ~$0.05 per Pro Image call (post-I/O
2026 pricing, subject to confirm). Four kit assets per
generation → ~$0.20 per kit.
- **Brand overlay second pass (optional, gemini-3-pro-image)** —
~$0.05 per asset re-rendered. If the user runs brand overlay
on all four assets: ~$0.20.
- **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.
- **TTS narration of capabilities panel (Gemini 3.1 Flash TTS)** —
~$10/M output tokens ≈ ~$0.000003/character. The capabilities
panel narration is ~600 chars → ~$0.002. Cached once per
locale; charged on first play per locale.
- **Expected per-kit cost (default flow, no brand overlay):**
~$0.24 — 1 sniff at ~$0.044, 1 headline polish at ~$0.0033,
4 Pro Image assets at ~$0.05 each.
- **Per-kit with brand overlay:** ~$0.44.
- **Per-regenerate-one-asset:** ~$0.05 (the sniff and the polish
are cached on the kit; only the Pro Image re-runs).
- **PDF composition:** server-side, no Gemini cost; just CPU on
Cloud Run. Negligible.
- **Storage:** Firebase Storage standard tier ~$0.026/GB/month.
A 4K PNG is ~6 MB; a kit (4 PNGs + PDF + zip) is ~30 MB →
~$0.0008/month per kit. 30-day expiry keeps the storage
bill near zero for free-tier users.
- **YouTube Data API v3:** 10,000 quota units per day free; one
`videos.list` call costs 1 unit. A free-tier deployment can
validate 10,000 URLs per day before quota concerns.
## 7. Design language
- **Mood:** A production studio at the moment between rendering
and shipping. Not a SaaS dashboard. Not a generator-tool with
geometric orbs. A small calm desktop with a paused YouTube
player on one side and a paper poster proof on the other, lamp
light pooling on a wooden surface. The visitor came in to make
one thing and leave; the app's job is to get out of the way.
- **Typography:** Clean grotesque for app chrome and data labels
(Inter or Geist). Display serif for the kit headline ("Kit
ready — Friday 22 August") and the section headings (Source
Serif Pro or Fraunces). The user's own headline, when echoed
back in the kit-ready view, sits in a condensed display sans
to read as a quoted billboard rather than UI copy.
- **Palette:** Warm desk-tone background `#F4EFE6` for the
surface, deep ink `#1B1714` for body text, brass `#A37430` for
primary CTAs and headings, dusk blue `#3B4A6B` for the URL
field and the sniff-preview chips, generation-pulse warm
amber `#D9A646` for the streaming state, faded red `#A33A2C`
for the not-a-reproduction disclaimer chrome and for any
validation error (private URL, age-restricted URL). Borrowed
from a print shop's swatch book, not from generator-tool
design systems.
- **Imagery:** Photographic. Lamp-lit desks, paper flyers, paused
phone screens, paste-boards of mock kits. No flat
illustrations. No floating orbs. No "AI is generating…"
cartoon glyphs. Generated via Nano Banana 2 with prompts
emphasising warm light, real paper, slight imperfection, no
people.
- **Hand-feel touches:** The URL field renders the validated
YouTube thumbnail inline (16:9 inside the field) the moment
the URL parses, so the visitor knows the app sees what they
meant. The sniff-preview palette chips have a subtle paint-
chip texture (rounded rectangles with a thin shadow, like
Pantone swatches). When a kit asset arrives, the tile fades
in over ~300 ms with a thin paper-shadow, as if a print was
pulled off the platen. The kit-ready view shows the 4 tiles
+ PDF arranged as a paste-board layout, not a grid — they
sit at slight rotations as if pinned by hand.
- **Spacing:** consistent 4-px base. Generous whitespace — the
paste page and the kit-ready view need air.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). The
URL field uses 12; the asset tiles use 6; the sniff-preview
modal uses 20.
- **Shadows:** subtle, layered, warm-tinted. Avoid heavy drop-
shadows. The kit-ready tiles use a soft paper-shadow under
each tile.
- **Motion:** purposeful — entrance fades on tile arrivals,
hover lifts, page transitions. Respect `prefers-reduced-
motion`. The sniff-preview palette chips appear with a 200 ms
stagger from left to right, the kit tiles fade in as they
arrive (not all at once). Reduced-motion: jump to final state.
No bouncing splash animations. No theatrical hero animations.
- **States:** every interactive element has hover, focus, active,
disabled. Loading uses skeletons that match the eventual tile
layout. Empty states have helpful next-action guidance
("Paste a YouTube URL above to start.").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic
placeholders like 'Your tagline here'.
- Invent plausible event headlines, venue names, dates, and
YouTube URLs that fit the domain (use the seed content in
section 8a as a starting point). When inventing, lean on
realistic event-promo patterns — a 7-piece soul band at an
inner-city club, a conference keynote in a hotel ballroom, an
indie film screening at a small festival — but never claim
that a fictional event is real or that a fictional venue
exists at a specific address.
- Tone: warm, direct, free of corporate language. This template
is for someone with one job and a deadline tonight, not for a
marketing department.
- 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 visitor
already speaks the jargon (concert promoters want to see
"DPI", "bleed", "crop marks" 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 sniff with
low confidence shows the palette chips at slightly reduced
saturation, with a tooltip "we read this video as ambiguous —
the kit may need a Nudge").
## 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 lineage of recent kits (sidebar):**
- "Brass Renaissance · Friday 22 August" — soul-band concert at
The Curtain Room. URL: `https://youtu.be/sample-brass` (use
a synthetic example URL; the seed-data layer mocks the
response).
- "Quiet Architectures · Sat 6 September" — chamber-music night
at a small recital hall. Reverent, low-contrast, velvet.
- "Hold Still · Indie Film Festival, October 12-16" — 4 film
posters with shared visual language.
- "Programme · LightWorks 2026 · Oct 28-30" — 38 conference
speaker promos, each from a different talk URL.
- "Annual Gala · The Hardwick Foundation · Nov 14" — charity
gala promo with brand overlay applied.
- "Service · St Anselm's · weekly" — community-service kit
with reverent register, regenerated weekly with the new
date.
**Sample kit in detail view (this is what the demo should
show):**
- **Kit name:** "Brass Renaissance — Friday 22 August"
- **Source URL:** a public YouTube URL of a soul-band
livestream (use a synthetic placeholder in the seed layer)
- **Event headline:** "Brass Renaissance"
- **Date:** 2026-08-22 (renders as "Friday 22 August" in EU
locale, "Friday, August 22" in US locale)
- **Venue:** "The Curtain Room"
- **Call-to-action:** "tickets from £14"
- **VideoBrief.palette:**
- primary `#C57B3A` (amber)
- secondary `#8B2A2A` (deep red)
- accent `#D4A24C` (brass)
- neutral_dark `#1B1410` (ink)
- neutral_light `#F4EFE6` (paper)
- **VideoBrief.lighting_register:** "warm"
- **VideoBrief.mood:** "celebratory"
- **VideoBrief.motion_language:** "rhythmic"
- **VideoBrief.dominant_texture:** "velvet"
- **VideoBrief.secondary_texture:** "brass"
- **VideoBrief.subject_signifier_one_line:** "silhouette of a
seven-piece brass band on a low stage, warm amber spill from
stage right"
- **VideoBrief.composition_language:** "wide static frame with
brass section centred, velvet curtain backdrop, soft spotlights
pooling on the players"
- **VideoBrief.rejected_likeness_note:** "no recognisable
performer faces or band logos surfaced — stylistic equivalents
only will follow"
- **VideoBrief.text_overlay_safe_zone:** "bottom_band"
- **VideoBrief.sniff_confidence:** 0.84
- **Generated headline variants:**
- 4:3 print poster (≤80): "Brass Renaissance · Friday 22 August · The Curtain Room"
- 1:1 square (≤45): "Brass Renaissance · 22 August"
- 16:9 banner (≤45): "Brass Renaissance · Friday 22 August"
- 9:16 story (≤30): "Brass Renaissance · 22 Aug"
- **Rendered assets:** five assets, each with a Firebase Storage
path (signed URLs returned to clients on demand),
generation_seconds in the 6-12 range, model_id_used
`"gemini-3-pro-image"`, generation_revision starts at 1.
- **PDF:** A2 portrait at 300 DPI, CMYK, with 3 mm bleed and
crop marks; the real-font headline overlays the model-rendered
headline in the bottom band.
- **not_a_reproduction_disclaimer:** "Inspired by the source
video. Not a reproduction. Performer likenesses are stylistic
equivalents, not endorsed or licenced."
**Sample event-input variations across the lineage:**
- Conference: headline "Programme · LightWorks 2026", date
2026-10-28 to 2026-10-30, venue "Conference Hall West",
call-to-action "Register at lightworks.example".
- Film festival: headline "Hold Still", date 2026-10-12,
venue "Curzon Indie", call-to-action "tickets at
curzon.example/holdstill".
- Charity gala: headline "Annual Gala · The Hardwick
Foundation", date 2026-11-14, venue "The Old Library",
call-to-action "give at hardwick.example/gala".
- Service: headline "Sunday Service", date varies weekly,
venue "St Anselm's", call-to-action "all welcome · 10 a.m.".
**Sample voice copy:**
- Landing tagline: "Paste a YouTube URL. Get back a poster, a
square, a banner, a story, and a print-ready PDF."
- URL field placeholder: "https://youtu.be/… (public videos
only)"
- Validation error (private URL): "This video is private or
unlisted — only public YouTube URLs work. Try a different
link, or set this one to public on YouTube first."
- Validation error (age-restricted): "This video is age-
restricted — we can't read age-gated content. Try a different
link."
- Generate button: "Generate kit"
- Sniff-preview heading: "Here's what we read in the video"
- Sniff-preview confirm: "Looks right — generate kit"
- Nudge button: "Nudge warmer" / "Nudge cooler"
- Generation status (per tile): "Rendering 4:3 print poster…"
/ "Rendering 1:1 Instagram square…" /
"Rendering 16:9 banner…" / "Rendering 9:16 story…"
- Kit-ready heading: "Kit ready — Brass Renaissance, Friday 22
August"
- Per-asset regenerate button: "Regenerate this one"
- Download CTA: "Download zip"
- Drive CTA: "Save kit to Drive"
- Share CTA: "Share preview link"
- Not-a-reproduction disclaimer (footer of kit-ready view):
"Inspired by the source video. Not a reproduction. Performer
likenesses are stylistic equivalents, not endorsed or licenced."
- Privacy panel: "Your URLs and event details are private to
you and anyone you explicitly share a kit with. We never train
on them."
**Sample input artefacts (for the build to demonstrate):**
- A synthetic public YouTube URL of a soul-band livestream
(use a mock response in the seed-data layer so the demo
doesn't depend on a real video).
- A pasted age-restricted URL that surfaces the precise
validation error.
- A pasted private URL that surfaces the precise validation
error.
- An event headline with an em-dash and an ampersand ("Brass
& Velvet — A Saturday Late") to demonstrate punctuation
preservation in the headline-polish call.
- A non-Latin script headline ("ব্রাস রেনেসাঁস") to
demonstrate the manual-review flag and the PDF-overlay
fallback for scripts the Pro Image model renders less
reliably.
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot
of a quiet desk at lamp-light, an A2 portrait flyer laid flat
showing a generic event poster (synthetic headline, synthetic
venue), a phone beside it showing a paused YouTube player on
a warm-lit stage. Generate via Nano Banana 2 with a prompt
emphasising "warm overhead lamp light, wooden desk, real
paper texture, A2 portrait flyer laid flat with placeholder
headline, phone showing paused YouTube player on a warm-lit
stage, no people, soft shadow under the paper".
- **App icon / wordmark:** Set in the display serif. Subtle
brass underline. No icon — just type.
- **Empty-state illustration:** A simple line drawing of a
paste-board with three blank flyer outlines pinned at
different angles. Hand-drawn aesthetic, not a flat icon.
Generate once at build time via Nano Banana 2
(`gemini-3.1-flash-image`), 1:1 WebP at 1024×1024, prompt:
"single hand-drawn ink line illustration of a wall-mounted
rectangular cork paste-board with three blank rectangular
flyer outlines pinned at slightly different angles (no
content inside the outlines), four small push-pins, off-
white paper background, slight pen imperfection, no
shading, no colour fill, no text, no commercial branding".
Ship as a seed asset at
`/public/samples/empty-state-pasteboard.webp`.
- **Demo thumbnails:** Generated per the prompts in section 8a
— Nano Banana 2 prompts that specifically request "a
paste-board pinned with three event flyers in different
aspect ratios, warm light through a studio window, no
people". Each demo thumbnail looks photographed, not
rendered.
- **Stock fallbacks:** If image generation fails, fall back to
the photographed sample paste-board from
`/public/samples/sample-pasteboard.jpg` (3:2 WebP, 2048×1365
— ship as a seed asset; recreate via Nano Banana 2
(`gemini-3.1-flash-image`) with the prompt: "photographic
cork paste-board pinned with three generic event flyers in
different aspect ratios (A2 portrait, square 1:1, 16:9
landscape), each flyer showing placeholder fictional event
text rendered legibly, warm light through a studio window
at golden hour, slight shadow under each flyer, no people,
no real brand logos, no commercial branding, real paper
texture"). Never to a "🎫" emoji.
- **Generated kit assets:** the four kit PNGs are the primary
output of the app. They are not "media assets" of the app
itself — they are the product. Treat their storage,
versioning, and signed-URL minting with the same care as a
print shop treats client artwork.
- **Generated imagery:** prefer Nano Banana 2 / Pro over stock
photography. Prompt for warmth, asymmetry, and slight
imperfection — avoid the glossy 'AI render' look.
- **Optimisation:** WebP/AVIF for thumbnails in the recent-kits
grid, `loading="lazy"`, explicit `width`/`height` to prevent
layout shift. The 4K kit PNGs ship as PNGs (lossless),
fronted by a smaller WebP preview in the kit-ready view.
- **Icons:** `lucide-react` for UI. Use sparingly — never
decorative-only.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and
disabled states.
- Forms validate inline and show specific error messages (not
"Invalid input"). "We can't reach this URL — it might be
private or unlisted. Try a different link." is the right
shape.
- Loading states use skeletons that match the eventual layout,
not spinners. The sniff-preview state shows skeleton palette
chips for ~3-4 seconds while the sniff runs.
- Empty states explain the next action with a button whose
label fits THIS app's domain: "Paste a YouTube URL above to
start", "Try with the sample URL", "Re-render this asset" —
never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- All AI-generated content streams in where supported. The
sniff returns in one shot (it's <1s of streaming); the
headline-polish returns token-by-token; the Pro Image calls
return per-asset as each completes (the kit-ready grid
populates tile-by-tile over ~25 seconds).
- If a Pro Image call fails, show a calm, specific error ("The
4:3 print poster didn't render — try Regenerate, or check the
URL is still public.") and offer per-asset retry.
- If the YouTube URL fails validation, the URL field shows the
precise reason inline (private, unlisted, age-restricted,
members-only, or "we can't find this video — check the URL").
- Low-confidence sniff (confidence < 0.6) shows the palette chips
at slightly reduced saturation with a tooltip "we read this
video as ambiguous — the kit may need a Nudge".
- Per-asset regenerate shows a tile-level skeleton during the
re-render; the other tiles stay put.
- The not-a-reproduction disclaimer surfaces as a non-dismissable
inline note at the foot of the kit-ready view and inside the
export metadata — not a toast that scrolls off-screen.
## 11. Tech & responsive requirements
- **Stack:** React + TypeScript + Tailwind CSS. Functional
components + hooks. Use Shadcn UI primitives where
appropriate. The kit-ready grid renders the 4 tiles + PDF
preview as a CSS grid with `grid-template-areas` for a
paste-board layout that adapts to viewport width.
- **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. **Free 2-app Cloud Run deploy from AI Studio
Build** is the default deployment path (post-I/O 2026
addition).
- **Model selection:** explicitly pin `gemini-3.5-flash` for
sniff and headline-polish; `gemini-3-pro-image` for kit
generation; `gemini-3.1-flash-image` for hero / empty-state;
`gemini-3.1-flash-tts-preview` for accessibility narration.
Set `thinkingLevel` explicitly per call. Omit
`thinkingConfig` on image-gen and TTS calls — the field is
not supported on those models.
- **Video reference:** pass the YouTube URL via the
`videoMetadata` / `fileData` part with
`mimeType: "video/youtube"` and `fileUri` set to the URL.
Do NOT paste the URL into the text prompt.
- **Pre-validation (URL pre-flight + .mp4 fallback):**
server-side YouTube oEmbed call (or YouTube Data API v3 if a
key is configured) before any Gemini call. The pre-flight
HEAD-checks (a) not 404, (b) not private, (c) not
geo-restricted from the server region, (d) embedding not
disabled, (e) not age-gated. If pre-flight fails, the UI
surfaces an elegant fallback modal: "This video can't be read
directly — upload the .mp4 (or paste a different URL)." The
fallback file flows through the Gemini Developer API Files
API and the resulting `files/*` resource name replaces the
YouTube URL via `fileData: { fileUri, mimeType: "video/mp4" }`
in downstream prompts.
- **Parallel asset generation:** the four kit assets generate
in parallel from a single server orchestration call to keep
total kit time around 25 seconds rather than 50+ serial.
- **PDF composition:** server-side using `pdf-lib` or
`pdfkit`. The Pro Image PNG is embedded as the photographic
background; the headline is overlaid as a real font (Google
Fonts API resolves the chosen font) so the printer gets
vector type. The model-rendered headline pixels are masked
in the PDF layer.
- **Database:** Firestore (auto-provisioned by AI Studio
Build). Show the seed kit 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 kit assets. Pre-signed URLs
only. 30-day default expiry; signed-in users can pin kits.
- **Workspace integration:** post-I/O 2026 — Workspace users
signed in via Firebase Auth (Google) get a "Save kit to
Drive" button that uses the new Workspace-without-OAuth
integration. No additional OAuth handshake required.
- **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 kit-ready view so per-asset regenerates update in place.
- Optimistic UI on writes; reconcile on response.
- Free-tier rate limit: 3 kits per signed-in user per day, 1
kit per anonymous session. Enforce server-side.
- **Local save fallback (FSA non-Chromium):** the "download
flyer PDF / individual asset" affordance uses
`showSaveFilePicker()` on Chromium; on Safari / Firefox, fall
back to an `` element pointed at a blob URL — FSA
`showSaveFilePicker()` is not supported there.
## 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. The not-a-reproduction disclaimer and validation-
error chrome use the faded-red `#A33A2C` against the warm
paper `#F4EFE6` — verify contrast meets AA.
- All images have meaningful `alt` text. The hero image has
`alt` describing the scene ("a quiet desk at lamp-light with
an A2 event flyer laid flat, a phone beside it showing a
paused YouTube player"). The generated kit assets have `alt`
composed of the event headline + the aspect ratio + the kit
date.
- Form fields have associated `