# Phase 1: Analyze Pipeline - Context

**Gathered:** 2026-05-20
**Status:** Ready for planning

<domain>
## Phase Boundary

Wire Gemini into `POST /api/analyze`. Phase 0 already delivers `{ bytes, mimeType, sizeBytes, fileName? }` from a public Drive URL. This phase transforms those bytes into a Zod-valid `{ person, transcript }` response via a **single** Gemini call (vision + transcript together) and returns it from the existing endpoint.

Phase is delivered when: `POST /api/analyze` with any of the 6 reference Drive URLs returns HTTP 200 with a schema-valid response, and edge cases (no person, no speech, non-English speech) return correctly nulled fields.

</domain>

<decisions>
## Implementation Decisions

### Model
- **D-01:** Use `gemini-2.5-flash` for the analyze call. Newest flash tier, identical cost/speed to 2.0-flash, better visual reasoning on demographic labels.

### Prompt Strategy
- **D-02:** Place rules in `systemInstruction` field on the `generateContent` request. User message contains only the file reference + a one-line "analyze this creative" trigger.
- **D-03:** Zero-shot — no few-shot examples. `responseSchema` already locks the JSON shape; rules in `systemInstruction` cover the content semantics.
- **D-04:** System instruction must encode all four assignment rules explicitly:
  1. Analyze only the person *in focus*; ignore background figures, passers-by, statisticians.
  2. If no person is in the frame, return `person: null` (but transcript may still be non-null for voice-over).
  3. Transcript: return spoken speech in the **original language**. No translation. `null` if no speech (image, music-only, silence).
  4. Per-attribute uncertainty → field-level `null` over guessing. Better to admit ambiguity than emit a wrong label.
- **D-05:** Prompt language: English. Vision/ASR quality is higher with English-language prompts; output is independent.

### Schema (single source of truth)
- **D-06:** 7 person fields locked as **placeholders** in [src/schema.ts](../../../src/schema.ts):
  `gender, age_range, ethnicity, hair_color, body_type, expression, setting`. Marketer-relevant set — what dating-vertical reviewers actually want to filter on.
- **D-07:** [src/schema.ts](../../../src/schema.ts) is the single source. Define:
  - The Zod schema (`personSchema`, `analyzeResponseSchema`)
  - The Gemini `responseSchema` (OpenAPI-ish JSON, generated or hand-written from the same field list)
  - The list of field labels passed into the system instruction
  Updating one constant (the field list) must propagate to all three. Acceptable: a single `PERSON_FIELDS` const that all three consumers read; or generate the JSON Schema from the Zod schema via `zod-to-json-schema`.
- **D-08:** All 7 fields are `string | null` for the placeholder. No enums in v1 — we don't know the customer's canonical value set, and `string | null` lets Gemini emit whatever it observes. Tighten to enums after customer confirmation.

### Files API Lifecycle
- **D-09:** Use Files API for **both** image and video (single code path).
- **D-10:** Polling for video: every 2 seconds, max 15 attempts (30s timeout). Image uploads return `ACTIVE` immediately — no polling.
- **D-11:** Skip explicit `DELETE` after `generateContent` — Gemini auto-expires uploaded files at 48h. Cleanup adds a subrequest + a failure mode without quota savings.

### Error Handling
- **D-12:** Mirror the `DriveError` pattern: introduce `GeminiError extends Error` with a `status` field (HTTP code). Map to JSON response via the existing `jsonError` helper in `src/index.ts`.
- **D-13:** Distinguish (a) input-validation errors (`400`), (b) Gemini upstream failures (`502`), (c) Gemini quota/rate-limit (`429` pass-through if upstream returns 429, else `502`), (d) Files API processing timeout (`504`). Anything unexpected → `500`.
- **D-14:** No automatic retry in v1. Manual retry by re-sending the request is fine. Add retry-with-backoff only if multiple of the 6 test files fail intermittently.

### Logging
- **D-15:** Continue the `console.log('event.subevent', { fields })` pattern from `drive.ts`. Key events: `gemini.upload.start`, `gemini.upload.done`, `gemini.poll.attempt`, `gemini.generate.start`, `gemini.generate.done`, `gemini.parse.fail`. Visible via `wrangler tail`.

### Claude's Discretion
- The exact `gemini.ts` module shape (functions vs class). Functions feel right (stateless), but planner may pick differently.
- Whether to inline the JSON `responseSchema` literal vs generate from Zod via `zod-to-json-schema`. Both work; pick whichever is easier to keep in sync.
- The wording of the system instruction. Capture rules D-04, write English, iterate against the 6 test files. If we can hit ≥5/6 cleanly on first pass, ship; otherwise tighten one rule at a time.
- Whether to expose raw Gemini timings in the response (`elapsedMs`) — useful for the README write-up but not required.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Project context (this repo)
- [.planning/PROJECT.md](../../PROJECT.md) — Core value, constraints, key decisions; "What This Is" and "Out of Scope" anchors
- [.planning/REQUIREMENTS.md](../../REQUIREMENTS.md) §"Analyze Pipeline (ANL)" — ANL-01..05 acceptance criteria; v2/OOS exclusions
- [.planning/ROADMAP.md](../../ROADMAP.md) §"Phase 1: Analyze Pipeline" — 5 success criteria, key risks
- [src/schema.ts](../../../src/schema.ts) — Current placeholder Zod (will be refactored to be the single source)
- [src/types.ts](../../../src/types.ts) — `Env` interface; `GEMINI_API_KEY` binding already declared
- [src/drive.ts](../../../src/drive.ts) — `DriveFile` interface (`{ bytes, mimeType, sizeBytes, fileName? }`) — the input to the Gemini call. `DriveError` pattern is the template for `GeminiError`.
- [src/index.ts:26](../../../src/index.ts) — `handleAnalyze` — Gemini call inserted between `downloadFromDrive` and the JSON response

### Gemini API documentation
- Gemini Files API (upload, get, list, delete): https://ai.google.dev/gemini-api/docs/files
- Gemini structured output (`responseMimeType`, `responseSchema`): https://ai.google.dev/gemini-api/docs/structured-output
- Gemini system instructions: https://ai.google.dev/gemini-api/docs/text-generation#system-instructions
- Gemini model catalog (`gemini-2.5-flash` capabilities, limits, pricing): https://ai.google.dev/gemini-api/docs/models
- Video understanding (FileData reference, supported formats): https://ai.google.dev/gemini-api/docs/video-understanding

### Cloudflare Workers
- Workers fetch limits (subrequests, response sizes): https://developers.cloudflare.com/workers/platform/limits/
- `wrangler tail` for live logs: https://developers.cloudflare.com/workers/observability/logs/tail/

### Assignment original text
- Captured verbatim in the session kickoff doc (no separate file — quoted in PROJECT.md context section). Key constraints: 5–6h budget, single-call preferred, "Качість промпту суттєво впливає на оцінку".

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- **`analyzeRequestSchema`** in [src/schema.ts](../../../src/schema.ts) — already validates `{ url: string }`. Reuse as-is.
- **`analyzeResponseSchema`** in [src/schema.ts](../../../src/schema.ts) — placeholder, refactor in this phase to include the 7 fields + `transcript: string | null`.
- **`jsonError(status, message, details?)`** in [src/index.ts](../../../src/index.ts) — uniform error response shape; `GeminiError` plugs into it.
- **`DriveError` class pattern** in [src/drive.ts](../../../src/drive.ts) — `extends Error`, carries `status`. Copy for `GeminiError`.
- **`console.log('event.x', {...})` structured-log pattern** in `drive.ts` — keeps `wrangler tail` readable.

### Established Patterns
- One module per external concern: `drive.ts` for Drive, `gemini.ts` (new) for Gemini. Index stays thin.
- Plain async functions exported by name (no classes). `downloadFromDrive` is the template.
- TypeScript strict + `noUncheckedIndexedAccess` enabled — array/Uint8Array indexing requires either length checks or nullish guards. Already proved fine in `drive.ts:sniffContentType`.
- Zod boundary validation: parse at every external trust boundary (incoming HTTP body, Gemini response).

### Integration Points
- [src/index.ts:26–48](../../../src/index.ts) `handleAnalyze` — current control flow:
  1. Parse JSON body
  2. `analyzeRequestSchema.safeParse`
  3. `downloadFromDrive(url)` → `DriveFile` *(or `DriveError`)*
  4. **NEW: `analyzeWithGemini(file)` → `AnalyzeResponse`** *(or `GeminiError`)*
  5. `Response.json(analyzeResponse)`
- `Env` interface in [src/types.ts](../../../src/types.ts) already declares `GEMINI_API_KEY: string`. `.dev.vars` already populated.

</code_context>

<specifics>
## Specific Ideas

- The assignment explicitly says video frame extraction is Gemini's job — pass the whole file via Files API, do not split frames in the Worker.
- Per-attribute null is preferred over a "best guess" — captured as D-04.4 because reviewers will scrutinise the README test table. A field nulled with a transparent reason is more defensible than a wrong label.
- Single source of truth for the 7 fields is non-negotiable: customer may send the real list at any moment, the swap must be a one-line change. D-07 makes this observable.
- The README test-result table from Phase 3 is the proof-of-quality artefact. Phase 1 success criteria should already produce data that maps cleanly into that table (per-file: status, person summary, transcript snippet).

</specifics>

<deferred>
## Deferred Ideas

- **Retry-with-backoff on transient Gemini errors** — only add if test runs show intermittent failure. Currently not justified by the 6 fixed test inputs.
- **Per-attribute confidence scores** — captured in v2 (V2-02), not in Phase 1.
- **Streaming response** — captured in v2 (V2-03).
- **KV cache by Drive fileId** — captured in v2 (V2-04). Real workflow probably calls same file many times during marketing review, but out of scope for the test.
- **Enum tightening for the 7 fields** — once customer confirms the canonical value set. Currently all `string | null`.
- **`elapsedMs` field in the response** — possible README enhancement, planner may include or not.

</deferred>

---

*Phase: 01-analyze-pipeline*
*Context gathered: 2026-05-20*
