## Step 3: services/transcription.py + providers/ 

### Objective

Implement the **AI transcription integration layer** so the app can:

1. Read the curated prompt from `PROMPT_DIR`
2. Send prompt + image to the configured provider (OpenRouter)
3. Return normalized transcription output (or structured failure)

This corresponds to MVP Step 3 from `docs/mvp.md`:
- `services/transcription.py`
- `providers/` adapter(s)

---

## Scope for Step 3

### In scope
- Provider abstraction and OpenRouter adapter
- Prompt file loading utility in service layer
- Image payload preparation
- One high-level transcription service function usable by Step 4 worker
- Unit tests (mocked provider SDK, no external calls)

### Out of scope
- Job polling/background loop (Step 4)
- DB status transition orchestration in worker loop (Step 4)
- UI invocation/wiring (Step 5)

---

## Planned Deliverables

### Source files
- `src/transcription/providers/base.py`
- `src/transcription/providers/openrouter.py`
- `src/transcription/providers/__init__.py` (exports + factory)
- `src/transcription/services/transcription.py`
- `src/transcription/services/__init__.py` (optional export)

### Tests
- `tests/providers/test_openrouter.py`
- `tests/services/test_transcription.py`

### Test directory convention
- Mirror source domains under `tests/`.
- Provider adapter tests live under `tests/providers/`.
- Service-layer tests live under `tests/services/`.
- Prefer one focused test module per production module (for Step 3: `test_openrouter.py`, `test_transcription.py`).

---

## Design Decisions (before coding)

1. **Provider interface first**
   - Define a stable contract independent of SDK specifics.
   - Prevent Step 4 from depending on raw SDK response shapes.

2. **Service returns normalized result object**
   - Include: `text`, `provider`, `model`, `raw_error`/exception metadata.
   - Worker can map this cleanly to `Transcript` and `JobStatus`.

3. **Prompt loaded from file at call time**
   - Uses `get_settings().prompt_dir / "transcribe_document.md"`.
   - Keeps prompt edits hot-swappable without code changes.

4. **Clear exception boundary**
   - SDK/network/model failures become predictable domain exceptions:
     - `ProviderError`
     - `PromptLoadError`
     - `TranscriptionError` (optional top-level wrapper)

5. **Model resolution policy**
   - Use `settings.provider_model` if set
   - Otherwise use adapter default constant (e.g., vision-capable model slug)

---

## Task-by-Task Execution Checklist

## Phase A — Provider contract

- [ ] Create `src/transcription/providers/base.py`
- [ ] Define protocol/ABC for transcription providers:
  - [ ] method signature accepts prompt text + image bytes (or data URL) + mime type
  - [ ] returns normalized text result (and optional metadata)
- [ ] Define shared provider exceptions:
  - [ ] `ProviderError`
  - [ ] optional subclasses (`ProviderAuthError`, `ProviderResponseError`)

---

## Phase B — OpenRouter adapter

- [ ] Create `src/transcription/providers/openrouter.py`
- [ ] Implement `OpenRouterTranscriptionProvider` with:
  - [ ] config-driven API key usage
  - [ ] optional referer/title attribution headers
  - [ ] model resolution fallback when `provider_model` is unset
- [ ] Implement request building:
  - [ ] prompt included as instruction content
  - [ ] image included in supported format for vision call
- [ ] Implement response parsing:
  - [ ] extract final transcript text from SDK response
  - [ ] validate non-empty text
- [ ] Wrap SDK failures into `ProviderError` with clean message

---

## Phase C — Provider factory

- [ ] Update `src/transcription/providers/__init__.py`
- [ ] Add `get_transcription_provider()` factory:
  - [ ] reads `settings.provider`
  - [ ] returns OpenRouter adapter for `openrouter`
  - [ ] raises explicit error for unsupported provider values

---

## Phase D — Transcription service (Step 3 core)

- [ ] Create `src/transcription/services/transcription.py`
- [ ] Add prompt loader function:
  - [ ] default file: `transcribe_document.md`
  - [ ] raises `PromptLoadError` on missing/empty file
- [ ] Add image loader/validator:
  - [ ] path existence check
  - [ ] allowed mime detection (`.jpg/.jpeg/.png/.tiff/.pdf` policy aligned to MVP)
- [ ] Add high-level function (name example):
  - [ ] `transcribe_document_image(image_path, prompt_name="transcribe_document.md")`
  - [ ] loads prompt + image
  - [ ] calls provider from factory
  - [ ] returns normalized transcription result object
- [ ] Add structured logging at key boundaries:
  - [ ] prompt loaded
  - [ ] provider invoked
  - [ ] success/failure outcome (no sensitive data in logs)

---

## Phase E — Tests (two-phase scaffold -> fill)

### Required execution resources

Load and reference these directly during test planning/implementation so the two-phase flow is enforced:

- [ ] `resource://catalog/prompts/pytest-scaffold`
- [ ] `resource://prompts/pytest-scaffold/document`
- [ ] `resource://catalog/prompts/pytest-fill-scaffold`
- [ ] `resource://prompts/pytest-fill-scaffold/document`

### Phase E1 — Scaffold test structure first

Prompt: `resource://catalog/prompts/pytest-scaffold`

Suggested arguments:
- [ ] `target_modules` = `src/transcription/providers/openrouter.py`, `src/transcription/services/transcription.py`
- [ ] `mode` = `scaffold`
- [ ] `path_strategy` = `src-to-tests-mirror`
- [ ] `naming_style` = `concise-behavior`

Expected scaffold outcomes:
- [ ] `tests/providers/test_openrouter.py` exists with class/method skeletons and one-line docstrings
- [ ] `tests/services/test_transcription.py` exists with class/method skeletons and one-line docstrings
- [ ] collection succeeds on scaffold-only tests

Scaffold coverage targets:
- [ ] adapter initializes from settings
- [ ] model fallback when `provider_model is None`
- [ ] referer/title options included when set
- [ ] successful SDK response parses transcript text
- [ ] SDK exception maps to `ProviderError`
- [ ] empty/invalid response maps to `ProviderError`
- [ ] prompt loader reads canonical prompt file
- [ ] missing prompt raises `PromptLoadError`
- [ ] transcription function loads file and calls provider once
- [ ] image path missing raises clear error
- [ ] provider error is propagated/wrapped predictably
- [ ] returned result includes transcript text and metadata

### Phase E2 — Fill scaffolded tests with assertions

Prompt: `resource://catalog/prompts/pytest-fill-scaffold`

Suggested arguments:
- [ ] `target_files` = `tests/providers/test_openrouter.py`, `tests/services/test_transcription.py`
- [ ] `stack` = `pure-python`
- [ ] `strategy` = `minimal`
- [ ] `marker_lane` = `unit`

Fill constraints:
- [ ] preserve scaffold class/method names and one-line docstrings
- [ ] keep mocks to an absolute minimum; mock only network boundaries and non-deterministic failures
- [ ] keep one behavior target per test method

> Default suite should remain deterministic and fast, but mocking should be minimal and intentional.

### Optional real-endpoint validation lane

- [ ] Add an opt-in integration lane for real provider calls (for example `@pytest.mark.integration` and `@pytest.mark.live_api`).
- [ ] Gate live tests behind explicit env vars (for example `OPENROUTER_API_KEY`, optional `RUN_LIVE_API_TESTS=1`).
- [ ] Exclude live tests from default CI/local runs unless explicitly requested.
- [ ] Keep at least one thin smoke path that can validate request/response compatibility against the real endpoint.

---

## Phase F — Verification commands

- [ ] E1 scaffold validation: `uv run pytest --collect-only -q`
- [ ] E2 fill validation (unit lane): `uv run pytest -m unit -q`
- [ ] E2 targeted provider file: `uv run pytest tests/providers/test_openrouter.py -q`
- [ ] E2 targeted service file: `uv run pytest tests/services/test_transcription.py -q`
- [ ] E2 final full-suite check: `uv run pytest -q`

---

## Implementation Notes / Guardrails

- Avoid coupling Step 3 service to DB models directly (that belongs in Step 4 orchestration).
- Do not silently swallow provider errors.
- Keep prompt filename stable (`transcribe_document.md`) unless explicitly parameterized.
- Keep request/response normalization inside provider adapter, not worker/UI layers.

---

## Definition of Done (Step 3)

Step 3 is done when:

1. Provider abstraction exists and OpenRouter adapter is implemented.
2. Service can transcribe a local image using prompt file content.
3. Failures are returned as structured exceptions, not raw SDK traceback noise.
4. Unit tests for provider and service pass.
5. Full suite remains green under `uv run pytest -q`.
6. Step 4 can call a single service function to process queued jobs.
