generated from john/python-template
6.2 KiB
6.2 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/test_providers_openrouter.pytests/test_transcription_service.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 (mocked, deterministic)
tests/test_providers_openrouter.py
- test adapter initializes from settings
- test model fallback when
provider_model is None - test referer/title options are included when set
- test successful SDK response parses transcript text
- test SDK exception maps to
ProviderError - test empty/invalid response maps to
ProviderError
tests/test_transcription_service.py
- test prompt loader reads canonical prompt file
- test missing prompt raises
PromptLoadError - test transcription function loads file and calls provider once
- test image path missing raises clear error
- test provider error is propagated/wrapped predictably
- test returned result includes transcript text and metadata
Keep these unit tests mocked (no real OpenRouter calls in default suite).
Phase F — Verification commands
uv run pytest tests/test_providers_openrouter.py -quv run pytest tests/test_transcription_service.py -quv 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.
If you want, I can now convert this into a PR-ready markdown checklist (same format as Step 2) and then implement it once you confirm.