generated from john/python-template
8.5 KiB
8.5 KiB
Step 3: services/transcription.py + providers/
Objective
Implement the AI transcription integration layer so the app can:
- Read the curated prompt from
PROMPT_DIR - Send prompt + image to the configured provider (OpenRouter)
- Return normalized transcription output (or structured failure)
This corresponds to MVP Step 3 from docs/mvp.md:
services/transcription.pyproviders/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.pysrc/transcription/providers/openrouter.pysrc/transcription/providers/__init__.py(exports + factory)src/transcription/services/transcription.pysrc/transcription/services/__init__.py(optional export)
Tests
tests/providers/test_openrouter.pytests/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)
-
Provider interface first
- Define a stable contract independent of SDK specifics.
- Prevent Step 4 from depending on raw SDK response shapes.
-
Service returns normalized result object
- Include:
text,provider,model,raw_error/exception metadata. - Worker can map this cleanly to
TranscriptandJobStatus.
- Include:
-
Prompt loaded from file at call time
- Uses
get_settings().prompt_dir / "transcribe_document.md". - Keeps prompt edits hot-swappable without code changes.
- Uses
-
Clear exception boundary
- SDK/network/model failures become predictable domain exceptions:
ProviderErrorPromptLoadErrorTranscriptionError(optional top-level wrapper)
- SDK/network/model failures become predictable domain exceptions:
-
Model resolution policy
- Use
settings.provider_modelif set - Otherwise use adapter default constant (e.g., vision-capable model slug)
- Use
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
OpenRouterTranscriptionProviderwith:- config-driven API key usage
- optional referer/title attribution headers
- model resolution fallback when
provider_modelis 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
ProviderErrorwith 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
- reads
Phase D — Transcription service (Step 3 core)
- Create
src/transcription/services/transcription.py - Add prompt loader function:
- default file:
transcribe_document.md - raises
PromptLoadErroron missing/empty file
- default file:
- Add image loader/validator:
- path existence check
- allowed mime detection (
.jpg/.jpeg/.png/.tiff/.pdfpolicy 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-scaffoldresource://prompts/pytest-scaffold/documentresource://catalog/prompts/pytest-fill-scaffoldresource://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.pymode=scaffoldpath_strategy=src-to-tests-mirrornaming_style=concise-behavior
Expected scaffold outcomes:
tests/providers/test_openrouter.pyexists with class/method skeletons and one-line docstringstests/services/test_transcription.pyexists 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.pystack=pure-pythonstrategy=minimalmarker_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.integrationand@pytest.mark.live_api). - Gate live tests behind explicit env vars (for example
OPENROUTER_API_KEY, optionalRUN_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:
- Provider abstraction exists and OpenRouter adapter is implemented.
- Service can transcribe a local image using prompt file content.
- Failures are returned as structured exceptions, not raw SDK traceback noise.
- Unit tests for provider and service pass.
- Full suite remains green under
uv run pytest -q. - Step 4 can call a single service function to process queued jobs.