Files
transcription/docs/step3.md
T
2026-06-24 14:02:36 -05:00

6.2 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/test_providers_openrouter.py
  • tests/test_transcription_service.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 (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 -q
  • uv run pytest tests/test_transcription_service.py -q
  • 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.

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.