Files

8.5 KiB

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.