## Step 4: `services/upload.py` + `worker.py` ### Objective Implement the MVP upload and background-processing pipeline so the system can: 1. Save uploaded files into `UPLOAD_DIR` 2. Create `Document` + `Job(status="queued")` 3. Process queued jobs in a worker loop: - `queued -> processing` - call Step 3 transcription service - persist `Transcript` - finalize as `transcribed` or `failed` This step advances MVP Feature 1 + Feature 2 and supports REQ-1, REQ-2, REQ-3, REQ-4, REQ-6. --- ## Scope ### In scope - `src/transcription/services/upload.py` - `src/transcription/worker.py` - Upload persistence logic and initial job creation - Worker polling and single-job lifecycle execution - Deterministic test coverage for upload + worker (default suite) ### Out of scope - UI integration and pages (Step 5) - Queue infrastructure beyond in-process loop - Async DB/session architecture refactor - Broad production hardening beyond MVP needs --- ## Planned Deliverables ### Source files - `src/transcription/services/upload.py` - `src/transcription/worker.py` - `src/transcription/services/__init__.py` (export updates as needed) ### Test files - `tests/services/test_upload.py` - `tests/services/test_worker.py` ### Optional external lane (already present pattern) - reuse `external` marker for live-provider checks where appropriate - keep external out of default lane --- ## Required MCP Prompt References (for test workflow) Apply these resources directly during Step 4 test creation: 1. `resource://catalog/prompts/pytest-scaffold` 2. `resource://prompts/pytest-scaffold/document` 3. `resource://catalog/prompts/pytest-fill-scaffold` 4. `resource://prompts/pytest-fill-scaffold/document` And (as referenced by those prompts) apply relevant pytest skill references for: - naming/hierarchy - marker defaults - SQLAlchemy sync testing behavior where applicable --- ## Design Decisions 1. **Upload service owns initial file + record creation** - Writes file, creates `Document`, creates queued `Job`, returns IDs/path. 2. **Worker owns lifecycle transitions** - Worker is the single owner of `queued -> processing -> terminal` job state changes. 3. **Worker uses Step 3 service boundary** - Worker calls `transcribe_document_image(...)`; no provider-specific SDK logic in worker. 4. **Failure information is always persisted** - On failure: store `Transcript(text=None, error_detail=...)` and set `Job.status=failed`. 5. **Loop remains simple and stoppable** - In-process polling loop with stop event and poll interval for MVP simplicity and testability. --- ## Task-by-Task Execution Checklist ## Phase A — Implement upload service (`src/transcription/services/upload.py`) - [ ] Create `UploadError` exception - [ ] Create `UploadJobResult` dataclass with: - [ ] `document_id` - [ ] `job_id` - [ ] `stored_path` - [ ] `original_filename` - [ ] Add filename safety handling: - [ ] normalize to basename - [ ] avoid path traversal - [ ] collision-safe stored name (e.g., UUID prefix/suffix) - [ ] Validate upload payload: - [ ] non-empty bytes required - [ ] extension in supported set (`.jpg/.jpeg/.png/.tif/.tiff/.pdf`) - [ ] Ensure upload directory exists (`mkdir(parents=True, exist_ok=True)`) - [ ] Write file bytes to `UPLOAD_DIR` - [ ] Persist DB records in one transaction: - [ ] `Document(filename, file_path)` - [ ] `Job(document_id=..., status=queued)` - [ ] Return `UploadJobResult` - [ ] Add logging for success/failure boundaries --- ## Phase B — Implement worker core (`src/transcription/worker.py`) - [ ] Add `process_next_queued_job(...) -> bool` - [ ] Fetch oldest queued job - [ ] Return `False` when no queued jobs exist - [ ] Transition picked job to `processing` and update timestamp - [ ] Resolve associated `Document.file_path` - [ ] Call `transcribe_document_image(image_path=...)` - [ ] On success: - [ ] insert/update transcript text - [ ] clear error detail - [ ] mark job `transcribed` - [ ] update timestamp - [ ] On failure: - [ ] insert/update transcript with `text=None`, `error_detail=...` - [ ] mark job `failed` - [ ] update timestamp - [ ] Commit terminal state and return `True` - [ ] Add logs around job pickup, transition, and terminal outcome --- ## Phase C — Implement worker loop (`src/transcription/worker.py`) - [ ] Add `run_worker_loop(...)` - [ ] Accept configurable stop event/signal - [ ] Accept configurable poll interval - [ ] Repeatedly call `process_next_queued_job` - [ ] Sleep only when queue is empty - [ ] Exit cleanly when stop event is set --- ## Phase D — Exports - [ ] Update `src/transcription/services/__init__.py` to expose upload APIs - [ ] Keep existing transcription exports intact --- ## Phase E — Tests via MCP scaffold -> fill flow ## E1 Scaffold (structure only) Use scaffold prompt workflow first for: - `src/transcription/services/upload.py` - `src/transcription/worker.py` Expected scaffold targets: - `tests/services/test_upload.py` - `tests/services/test_worker.py` Scaffold rules: - [ ] Class hierarchy + method names + one-line docstrings only - [ ] No assertions or implementation details in scaffold phase - [ ] Keep method names concise and behavior-focused Validation: - [ ] `uv run pytest --collect-only -q` ## E2 Fill scaffold (implementation) Use fill prompt workflow for: - `tests/services/test_upload.py` - `tests/services/test_worker.py` - stack: `sqlalchemy-sync` (or `mixed` if combining pure + DB behaviors) - marker lane preference: `unit` and `integration` as appropriate - strategy: minimal deterministic implementation Fill rules (invariants): - [ ] Preserve scaffold class names, method names, and one-line docstrings - [ ] Do not rename/re-nest scaffolded tests unless explicitly approved - [ ] One behavior target per test - [ ] Minimal mocking; mock only network/nondeterministic boundaries Suggested test coverage: ### `tests/services/test_upload.py` - [ ] creates file + document + queued job (`integration`) - [ ] rejects empty bytes (`unit`) - [ ] rejects unsupported extension (`unit`) - [ ] writes collision-safe unique filename (`integration`) - [ ] persisted job status is `queued` (`integration`) ### `tests/services/test_worker.py` - [ ] returns `False` when queue empty (`integration`) - [ ] transitions `queued -> processing -> transcribed` on success (`integration`) - [ ] stores transcript text on success (`integration`) - [ ] transitions to `failed` and stores `error_detail` on failure (`integration`) - [ ] updates existing transcript instead of duplicate create (`integration`) - [ ] worker loop exits when stop event set (`unit`) --- ## Marker Strategy - `unit`: pure logic tests (filename handling, loop stop behavior, validation logic) - `integration`: DB + service orchestration tests (SQLite/session/contracts) - `external`: opt-in live provider tests only (not part of default Step 4 lane) No new marker needed; reuse existing marker registration. --- ## Validation Sequence (strict order) - [ ] `uv run pytest --collect-only -q` - [ ] `uv run pytest -m unit -q` *(if unit tests touched)* - [ ] `uv run pytest tests/services/test_upload.py -q` - [ ] `uv run pytest tests/services/test_worker.py -q` - [ ] `uv run pytest -q` --- ## Reporting Requirements (after implementation) Implementation report must include: 1. Files created/updated 2. Fixture and marker decisions 3. MCP references used and why 4. Validation command results 5. Remaining risks/open questions (only blockers) --- ## Guardrails - Keep Step 4 independent from UI concerns. - Do not call provider SDK directly from worker. - Do not silently swallow exceptions. - Always persist terminal job outcome. - Keep default suite deterministic and fast. - Preserve scaffold invariants during fill phase. --- ## Definition of Done (Step 4) Step 4 is complete when: - [ ] Upload service writes file and creates `Document` + queued `Job` - [ ] Worker processes queued jobs end-to-end using Step 3 transcription service - [ ] Success path persists transcript text and sets `transcribed` - [ ] Failure path persists error detail and sets `failed` - [ ] Queue-empty path returns cleanly - [ ] New tests pass and full suite is green (`uv run pytest -q`) - [ ] Output report includes MCP reference usage + validation evidence