generated from john/python-template
V1 mostly complete except for some testing. Linting in the last step changed nearly every file which is why this commit is so larger.
This commit is contained in:
+79
-248
@@ -1,299 +1,130 @@
|
|||||||
# Architecture
|
# Architecture (V1 Baseline)
|
||||||
|
|
||||||
This document describes the production architecture of the personal historical-document transcription system. The system is intentionally optimized for single-user operation, low operational overhead, and clean internal boundaries that support future growth without rewrites.
|
This document describes the current architecture of the personal historical-document transcription system and serves as the V1 technical baseline.
|
||||||
|
|
||||||
## Architecture Objectives
|
## Architecture Objectives
|
||||||
|
|
||||||
The production architecture is designed to:
|
- preserve source material as transcribed text
|
||||||
|
- keep operational complexity low for personal-scale deployment
|
||||||
|
- support asynchronous processing without external queue infrastructure
|
||||||
|
- maintain clear module boundaries for incremental extension
|
||||||
|
|
||||||
- preserve verbatim family-history source material as searchable text
|
## Runtime Topology
|
||||||
- keep operational complexity low for a personal deployment
|
|
||||||
- support asynchronous transcription without requiring distributed infrastructure
|
|
||||||
- maintain clear module boundaries so extensions can be added incrementally
|
|
||||||
|
|
||||||
## Production Scope And Scale
|
V1 runtime is a modular monolith:
|
||||||
|
|
||||||
The deployed system targets personal use and a corpus of several thousand documents processed over time. The architecture favors simple, composable building blocks over distributed orchestration.
|
- one FastAPI + NiceGUI application process
|
||||||
|
- one in-process async worker loop
|
||||||
Current scope includes:
|
- relational persistence via SQLModel (SQLite baseline)
|
||||||
|
|
||||||
- content source upload and metadata capture
|
|
||||||
- asynchronous transcription jobs
|
|
||||||
- prompt-library driven transcription behavior, with one Markdown file per prompt
|
|
||||||
- original transcription review and optional revision review
|
|
||||||
- full-text search over accepted transcripts
|
|
||||||
- export of transcript data
|
|
||||||
|
|
||||||
## Deployment Topology
|
|
||||||
|
|
||||||
The production deployment uses [Docker Compose](https://docs.docker.com/compose/) and treats containerized databases as extremely lightweight operational dependencies.
|
|
||||||
|
|
||||||
Running [PostgreSQL](https://www.postgresql.org/docs/) in its own container is considered simple by default for this system.
|
|
||||||
|
|
||||||
Running [MongoDB](https://www.mongodb.com/docs/) in its own container is also considered simple when document-centric storage is enabled.
|
|
||||||
|
|
||||||
Container count is not a hard architectural limit; a three-container deployment (app, PostgreSQL, MongoDB) is an acceptable baseline.
|
|
||||||
|
|
||||||
### Baseline Topology (Two Containers)
|
|
||||||
|
|
||||||
- one application container
|
|
||||||
- one PostgreSQL container
|
|
||||||
- embedded background worker execution inside the app process
|
|
||||||
|
|
||||||
### Expanded Topology (Three Containers)
|
|
||||||
|
|
||||||
- application container
|
|
||||||
- PostgreSQL container
|
|
||||||
- MongoDB container
|
|
||||||
|
|
||||||
No additional queue, scheduler, or search-engine containers are required in the baseline production setup.
|
|
||||||
|
|
||||||
## Runtime Architecture
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
User[Browser User] --> App[FastAPI + NiceGUI Service]
|
U[Browser User] --> A[FastAPI + NiceGUI App]
|
||||||
App --> Worker[In-process Background Worker]
|
A --> W[In-process Worker]
|
||||||
App --> PG[(PostgreSQL)]
|
A --> DB[(SQLite via SQLModel)]
|
||||||
App --> MG[(MongoDB Document Store)]
|
W --> P[OpenRouter Provider]
|
||||||
Worker --> AI[Transcription Provider]
|
W --> DB
|
||||||
Worker --> PG
|
|
||||||
Worker --> MG
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Runtime Ownership And Startup Policy
|
## Lifecycle Ownership
|
||||||
|
|
||||||
The current implementation now uses explicit lifespan-owned runtime resources.
|
Application lifespan owns runtime setup/teardown:
|
||||||
|
|
||||||
- application lifespan initializes and disposes database runtime resources
|
- configure logging
|
||||||
- worker lifecycle is owned by application lifespan startup/shutdown
|
- initialize and dispose DB runtime resources
|
||||||
- worker receives lifespan-owned database engine dependency explicitly
|
- optional schema bootstrap by environment policy
|
||||||
- schema bootstrap policy is environment-aware and explicit:
|
- recover stale processing jobs
|
||||||
- development/test default to bootstrap enabled
|
- start/stop worker consumer lifespan
|
||||||
- production defaults to bootstrap disabled
|
|
||||||
- explicit override is available via configuration
|
|
||||||
|
|
||||||
This aligns implementation toward REQ-7 and REQ-10 while preserving personal-scale operational simplicity.
|
|
||||||
|
|
||||||
## Layered Module Structure
|
## Layered Module Structure
|
||||||
|
|
||||||
### Interface Layer
|
### Interface Layer
|
||||||
|
|
||||||
Responsibility:
|
- `src/transcription/ui/**` (NiceGUI pages/components)
|
||||||
|
- `src/transcription/api/**` (FastAPI routes and error handlers)
|
||||||
|
|
||||||
- HTTP API and UI routes
|
### Application/Workflow Layer
|
||||||
- request/response validation
|
|
||||||
- status and result presentation
|
|
||||||
|
|
||||||
Out of scope:
|
- `src/transcription/services/workflows.py`
|
||||||
|
- `src/transcription/worker.py`
|
||||||
|
|
||||||
- business-rule enforcement
|
Responsibilities:
|
||||||
- data-access implementation
|
|
||||||
|
|
||||||
### Application Layer
|
- orchestration and status transitions
|
||||||
|
- retry/timeout behavior
|
||||||
|
- provider call coordination
|
||||||
|
|
||||||
Responsibility:
|
### Service Layer
|
||||||
|
|
||||||
- upload and job orchestration
|
- `src/transcription/services/*.py`
|
||||||
- state transitions and retry policy
|
|
||||||
- coordination across domain and infrastructure ports
|
|
||||||
|
|
||||||
Out of scope:
|
Responsibilities:
|
||||||
|
|
||||||
- provider-specific protocol details
|
- CRUD and transactional boundaries
|
||||||
- ORM or storage-specific logic
|
- domain-aligned persistence operations
|
||||||
|
|
||||||
### Domain Layer
|
|
||||||
|
|
||||||
Responsibility:
|
|
||||||
|
|
||||||
- verbatim transcription policy
|
|
||||||
- revision and provenance invariants
|
|
||||||
- confidence and annotation semantics
|
|
||||||
|
|
||||||
Out of scope:
|
|
||||||
|
|
||||||
- web framework concerns
|
|
||||||
- database and network I/O
|
|
||||||
|
|
||||||
### Infrastructure Layer
|
### Infrastructure Layer
|
||||||
|
|
||||||
Responsibility:
|
- `src/transcription/db/**` (runtime/session/bootstrap)
|
||||||
|
- `src/transcription/providers/**` (OpenRouter adapter)
|
||||||
- persistence adapters (PostgreSQL and MongoDB)
|
|
||||||
- transcription-provider adapter
|
|
||||||
|
|
||||||
Out of scope:
|
|
||||||
|
|
||||||
- business policy decisions
|
|
||||||
|
|
||||||
## Processing Workflow
|
## Processing Workflow
|
||||||
|
|
||||||
Production transcription flow:
|
1. User uploads a source file from the UI.
|
||||||
|
2. App persists `Document`, `Job(queued)`, and `Source`.
|
||||||
|
3. Worker claims next queued job and marks `processing`.
|
||||||
|
4. Worker calls provider with prompt + source bytes.
|
||||||
|
5. On success, app writes immutable `Job.text` and marks `transcribed`.
|
||||||
|
6. On failure, app writes `Job.error_detail` and marks `failed`.
|
||||||
|
7. UI exposes job detail, original transcription, and optional revision.
|
||||||
|
|
||||||
1. A user uploads one or more content sources through the UI or API.
|
## Domain Ownership Invariants
|
||||||
2. The application validates payloads and creates document, source, and job records.
|
|
||||||
3. The in-process worker de-queues the job and calls the transcription provider.
|
|
||||||
4. The application persists original transcription output on the job, plus confidence metadata and provenance events.
|
|
||||||
5. Job status transitions from queued to processing to transcribed or failed.
|
|
||||||
6. The UI and API expose status, optional revision to original transcription, and searchable transcription text.
|
|
||||||
|
|
||||||
## Data Model Ownership
|
- `Job.text` is immutable original provider output.
|
||||||
|
- `Revision` is optional, user-authored, and linked to `Source`.
|
||||||
|
- `Revision` does not overwrite original job transcription.
|
||||||
|
- Status lifecycle is fixed to: `queued -> processing -> transcribed|failed`.
|
||||||
|
|
||||||
System-of-record entities:
|
## Data Model Summary
|
||||||
|
|
||||||
- documents and content sources
|
- `Document` has many `Source` and many `Job`.
|
||||||
- transcription jobs, original transcription, and status events
|
- `Source` belongs to one `Document` and one `Job`.
|
||||||
- transcript revisions
|
- `Source` has optional `Revision` (`0..1`) enforced by unique `revision.source_id`.
|
||||||
- provenance metadata
|
|
||||||
|
|
||||||
### Original Transcription And Revision Ownership
|
## Simplicity Guardrails (V1)
|
||||||
|
|
||||||
- each processing job stores the original immutable provider output (`text`)
|
- no external queue/broker required
|
||||||
- provider metadata (`provider`, `model`, `prompt_name`) and failure detail (`error_detail`) are job-owned processing artifacts
|
- no search engine required
|
||||||
- revisions are optional user-authored edits linked to a content source
|
- no distributed worker fleet required
|
||||||
- a revision can be created from original `job.text`
|
- keep provider integration behind adapter boundary
|
||||||
- many jobs will have zero revisions; revisions are additive and never overwrite original provider output
|
|
||||||
- a document groups one or more content sources (images, PDFs, and future source types)
|
|
||||||
|
|
||||||
Storage strategy:
|
|
||||||
|
|
||||||
- PostgreSQL for relational system-of-record entities
|
|
||||||
- MongoDB for document-oriented payloads and large transcription artifacts
|
|
||||||
- versioned prompt artifacts stored as individual Markdown files for human editing and refinement
|
|
||||||
- in-memory execution state treated as ephemeral
|
|
||||||
|
|
||||||
## Transcription Prompt Asset Policy
|
|
||||||
|
|
||||||
The production system treats transcription prompts as maintainable content assets.
|
|
||||||
|
|
||||||
- each transcription prompt is stored in its own Markdown file
|
|
||||||
- prompt files are designed for direct human editing and iterative refinement
|
|
||||||
- prompt updates are independent and do not require bundling unrelated prompt changes
|
|
||||||
- prompt file identity and revision history are tracked through normal repository version control
|
|
||||||
|
|
||||||
## Simplicity Guardrails
|
|
||||||
|
|
||||||
The production system enforces these constraints to prevent accidental over-engineering:
|
|
||||||
|
|
||||||
- PostgreSQL in a container is treated as a lightweight default dependency
|
|
||||||
- MongoDB in a container is treated as a lightweight optional dependency
|
|
||||||
- three containers (app, PostgreSQL, MongoDB) is an acceptable simple deployment
|
|
||||||
- no dedicated queue or search cluster is introduced without measured need
|
|
||||||
- external infrastructure is added only behind existing ports/adapters
|
|
||||||
|
|
||||||
## Extension Path
|
## Extension Path
|
||||||
|
|
||||||
The architecture supports additive growth without changing domain contracts.
|
### V1 (current)
|
||||||
|
|
||||||
### Stage 1: Foundation (Current)
|
- SQLite baseline
|
||||||
|
- OpenRouter provider
|
||||||
|
- in-process worker
|
||||||
|
- optional single revision workflow
|
||||||
|
|
||||||
- upload, transcription, review, search, export
|
### V2 (planned)
|
||||||
- in-process worker execution
|
|
||||||
- single provider adapter
|
|
||||||
- app plus PostgreSQL deployment
|
|
||||||
|
|
||||||
### Stage 2: Throughput Hardening
|
- PostgreSQL as relational baseline
|
||||||
|
- optional MongoDB adjunct store for scoped use cases
|
||||||
|
- migration-first schema evolution
|
||||||
|
|
||||||
- optional MongoDB document-store enablement
|
See [ver2/ver2.md](ver2/ver2.md) for roadmap details.
|
||||||
- optional external worker/queue process
|
|
||||||
- stronger retry and dead-letter handling
|
|
||||||
|
|
||||||
### Stage 3: Intelligence Features
|
|
||||||
|
|
||||||
- entity extraction and cross-document linking
|
|
||||||
- timeline and narrative assembly
|
|
||||||
- optional multi-provider routing
|
|
||||||
|
|
||||||
Each stage preserves existing module boundaries and keeps migration risk low.
|
|
||||||
|
|
||||||
## Test Strategy
|
## Test Strategy
|
||||||
|
|
||||||
The test strategy is aligned to personal-scale operation with fast, deterministic feedback.
|
- unit tests for model/service behaviors
|
||||||
|
- integration tests for upload/workflow reliability
|
||||||
|
- UI integration tests for page/render contracts
|
||||||
|
- external provider tests opt-in via marker/config
|
||||||
|
|
||||||
### Unit Tests
|
## Related References
|
||||||
|
|
||||||
- domain transcription rules and annotation behavior
|
- [index.md](index.md)
|
||||||
- revision-history invariants
|
- [requirements.md](requirements.md)
|
||||||
- job state-transition logic
|
- [schema.md](schema.md)
|
||||||
|
- [error_handling.md](error_handling.md)
|
||||||
### Integration Tests
|
|
||||||
|
|
||||||
- repository behavior and transaction boundaries
|
|
||||||
- persistence-adapter and provider adapter contract mapping
|
|
||||||
- upload-to-persistence roundtrip
|
|
||||||
|
|
||||||
### End-to-End Tests
|
|
||||||
|
|
||||||
- happy path: upload, transcribe, review, search, export
|
|
||||||
- failure path: provider error, retry, surfaced failed status
|
|
||||||
|
|
||||||
### CI Execution Model
|
|
||||||
|
|
||||||
- fast suite on each push
|
|
||||||
- optional slower provider-sandbox checks on scheduled runs
|
|
||||||
|
|
||||||
## Risks And Controls
|
|
||||||
|
|
||||||
### Runtime Responsiveness
|
|
||||||
|
|
||||||
Risk:
|
|
||||||
|
|
||||||
- long jobs can reduce responsiveness in a single-process deployment
|
|
||||||
|
|
||||||
Control:
|
|
||||||
|
|
||||||
- bounded concurrency and visible job status in the UI
|
|
||||||
|
|
||||||
### Database Concurrency Limits
|
|
||||||
|
|
||||||
Risk:
|
|
||||||
|
|
||||||
- contention can appear under sustained concurrent writes in personal-scale infrastructure
|
|
||||||
|
|
||||||
Control:
|
|
||||||
|
|
||||||
- tuned connection pooling and phased use of MongoDB for document-heavy workloads
|
|
||||||
|
|
||||||
### Provider Output Variance
|
|
||||||
|
|
||||||
Risk:
|
|
||||||
|
|
||||||
- transcription quality varies by content source type, handwriting legibility, and source quality
|
|
||||||
|
|
||||||
Control:
|
|
||||||
|
|
||||||
- first-class human review and immutable revision history
|
|
||||||
|
|
||||||
## Technology References
|
|
||||||
|
|
||||||
- [FastAPI documentation](https://fastapi.tiangolo.com/)
|
|
||||||
- [NiceGUI documentation](https://nicegui.io/documentation)
|
|
||||||
- [Docker Compose documentation](https://docs.docker.com/compose/)
|
|
||||||
- [PostgreSQL documentation](https://www.postgresql.org/docs/)
|
|
||||||
- [MongoDB documentation](https://www.mongodb.com/docs/)
|
|
||||||
|
|
||||||
## Related Local References
|
|
||||||
|
|
||||||
- [System overview](index.md)
|
|
||||||
|
|
||||||
## Glossary
|
|
||||||
|
|
||||||
- Adapter: A component that translates between internal interfaces and external systems such as databases or AI services.
|
|
||||||
- Background job: Work executed outside the request/response path so the UI remains responsive.
|
|
||||||
- Boundary: A strict separation between modules with different responsibilities.
|
|
||||||
- CI (Continuous Integration): Automated test execution for code changes.
|
|
||||||
- Contract test: A test that verifies an adapter follows expected input/output behavior at a boundary.
|
|
||||||
- Domain layer: The module that contains core business rules and invariants.
|
|
||||||
- End-to-end test: A test that validates a full user flow across the running system.
|
|
||||||
- Full-text search: Text indexing and querying optimized for natural-language search.
|
|
||||||
- In-process worker: A background executor that runs within the same application process.
|
|
||||||
- Integration test: A test that verifies interactions between real modules and infrastructure components.
|
|
||||||
- MongoDB: A document-oriented database used for flexible, high-variance data structures.
|
|
||||||
- Modular monolith: A single deployable application with strongly separated internal modules.
|
|
||||||
- Port/Interface: A stable contract used by application/domain code to call infrastructure implementations.
|
|
||||||
- Prompt artifact: A single Markdown file that defines one transcription prompt and can be revised independently.
|
|
||||||
- Provenance: Metadata that records where generated data came from and how it was produced.
|
|
||||||
- Revision history: Optional versioned record of user-authored transcription edits over time.
|
|
||||||
- System of record: The authoritative persistent store for canonical data.
|
|
||||||
- Vertical slice: A minimal end-to-end feature path spanning UI/API, application logic, and persistence.
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# V2 Archive
|
||||||
|
|
||||||
|
This folder preserves pre-V1-alignment versions of core documentation that included planned target-state architecture material.
|
||||||
|
|
||||||
|
Archived snapshots:
|
||||||
|
|
||||||
|
- `index.pre-v1-alignment.md`
|
||||||
|
- `requirements.pre-v1-alignment.md`
|
||||||
|
- `architecture.pre-v1-alignment.md`
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- keep a durable reference for planned architecture language
|
||||||
|
- reduce risk of losing useful V2 direction while V1 docs stay implementation-aligned
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- These files are historical snapshots, not the active V1 source of truth.
|
||||||
|
- Active V1 docs remain at:
|
||||||
|
- `docs/index.md`
|
||||||
|
- `docs/requirements.md`
|
||||||
|
- `docs/architecture.md`
|
||||||
|
- V2 planning should continue in `docs/ver2/ver2.md` and related V2 artifacts.
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
This document describes the production architecture of the personal historical-document transcription system. The system is intentionally optimized for single-user operation, low operational overhead, and clean internal boundaries that support future growth without rewrites.
|
||||||
|
|
||||||
|
## Architecture Objectives
|
||||||
|
|
||||||
|
The production architecture is designed to:
|
||||||
|
|
||||||
|
- preserve verbatim family-history source material as searchable text
|
||||||
|
- keep operational complexity low for a personal deployment
|
||||||
|
- support asynchronous transcription without requiring distributed infrastructure
|
||||||
|
- maintain clear module boundaries so extensions can be added incrementally
|
||||||
|
|
||||||
|
## Production Scope And Scale
|
||||||
|
|
||||||
|
The deployed system targets personal use and a corpus of several thousand documents processed over time. The architecture favors simple, composable building blocks over distributed orchestration.
|
||||||
|
|
||||||
|
Current scope includes:
|
||||||
|
|
||||||
|
- content source upload and metadata capture
|
||||||
|
- asynchronous transcription jobs
|
||||||
|
- prompt-library driven transcription behavior, with one Markdown file per prompt
|
||||||
|
- original transcription review and optional revision review
|
||||||
|
- full-text search over accepted transcripts
|
||||||
|
- export of transcript data
|
||||||
|
|
||||||
|
## Deployment Topology
|
||||||
|
|
||||||
|
The production deployment uses [Docker Compose](https://docs.docker.com/compose/) and treats containerized databases as extremely lightweight operational dependencies.
|
||||||
|
|
||||||
|
Running [PostgreSQL](https://www.postgresql.org/docs/) in its own container is considered simple by default for this system.
|
||||||
|
|
||||||
|
Running [MongoDB](https://www.mongodb.com/docs/) in its own container is also considered simple when document-centric storage is enabled.
|
||||||
|
|
||||||
|
Container count is not a hard architectural limit; a three-container deployment (app, PostgreSQL, MongoDB) is an acceptable baseline.
|
||||||
|
|
||||||
|
### Baseline Topology (Two Containers)
|
||||||
|
|
||||||
|
- one application container
|
||||||
|
- one PostgreSQL container
|
||||||
|
- embedded background worker execution inside the app process
|
||||||
|
|
||||||
|
### Expanded Topology (Three Containers)
|
||||||
|
|
||||||
|
- application container
|
||||||
|
- PostgreSQL container
|
||||||
|
- MongoDB container
|
||||||
|
|
||||||
|
No additional queue, scheduler, or search-engine containers are required in the baseline production setup.
|
||||||
|
|
||||||
|
## Runtime Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
User[Browser User] --> App[FastAPI + NiceGUI Service]
|
||||||
|
App --> Worker[In-process Background Worker]
|
||||||
|
App --> PG[(PostgreSQL)]
|
||||||
|
App --> MG[(MongoDB Document Store)]
|
||||||
|
Worker --> AI[Transcription Provider]
|
||||||
|
Worker --> PG
|
||||||
|
Worker --> MG
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Ownership And Startup Policy
|
||||||
|
|
||||||
|
The current implementation now uses explicit lifespan-owned runtime resources.
|
||||||
|
|
||||||
|
- application lifespan initializes and disposes database runtime resources
|
||||||
|
- worker lifecycle is owned by application lifespan startup/shutdown
|
||||||
|
- worker receives lifespan-owned database engine dependency explicitly
|
||||||
|
- schema bootstrap policy is environment-aware and explicit:
|
||||||
|
- development/test default to bootstrap enabled
|
||||||
|
- production defaults to bootstrap disabled
|
||||||
|
- explicit override is available via configuration
|
||||||
|
|
||||||
|
This aligns implementation toward REQ-7 and REQ-10 while preserving personal-scale operational simplicity.
|
||||||
|
|
||||||
|
## Layered Module Structure
|
||||||
|
|
||||||
|
### Interface Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- HTTP API and UI routes
|
||||||
|
- request/response validation
|
||||||
|
- status and result presentation
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- business-rule enforcement
|
||||||
|
- data-access implementation
|
||||||
|
|
||||||
|
### Application Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- upload and job orchestration
|
||||||
|
- state transitions and retry policy
|
||||||
|
- coordination across domain and infrastructure ports
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- provider-specific protocol details
|
||||||
|
- ORM or storage-specific logic
|
||||||
|
|
||||||
|
### Domain Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- verbatim transcription policy
|
||||||
|
- revision and provenance invariants
|
||||||
|
- confidence and annotation semantics
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- web framework concerns
|
||||||
|
- database and network I/O
|
||||||
|
|
||||||
|
### Infrastructure Layer
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- persistence adapters (PostgreSQL and MongoDB)
|
||||||
|
- transcription-provider adapter
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- business policy decisions
|
||||||
|
|
||||||
|
## Processing Workflow
|
||||||
|
|
||||||
|
Production transcription flow:
|
||||||
|
|
||||||
|
1. A user uploads one or more content sources through the UI or API.
|
||||||
|
2. The application validates payloads and creates document, source, and job records.
|
||||||
|
3. The in-process worker de-queues the job and calls the transcription provider.
|
||||||
|
4. The application persists original transcription output on the job, plus confidence metadata and provenance events.
|
||||||
|
5. Job status transitions from queued to processing to transcribed or failed.
|
||||||
|
6. The UI and API expose status, optional revision to original transcription, and searchable transcription text.
|
||||||
|
|
||||||
|
## Data Model Ownership
|
||||||
|
|
||||||
|
System-of-record entities:
|
||||||
|
|
||||||
|
- documents and content sources
|
||||||
|
- transcription jobs, original transcription, and status events
|
||||||
|
- transcript revisions
|
||||||
|
- provenance metadata
|
||||||
|
|
||||||
|
### Original Transcription And Revision Ownership
|
||||||
|
|
||||||
|
- each processing job stores the original immutable provider output (`text`)
|
||||||
|
- provider metadata (`provider`, `model`, `prompt_name`) and failure detail (`error_detail`) are job-owned processing artifacts
|
||||||
|
- revisions are optional user-authored edits linked to a content source
|
||||||
|
- a revision can be created from original `job.text`
|
||||||
|
- many jobs will have zero revisions; revisions are additive and never overwrite original provider output
|
||||||
|
- a document groups one or more content sources (images, PDFs, and future source types)
|
||||||
|
|
||||||
|
Storage strategy:
|
||||||
|
|
||||||
|
- PostgreSQL for relational system-of-record entities
|
||||||
|
- MongoDB for document-oriented payloads and large transcription artifacts
|
||||||
|
- versioned prompt artifacts stored as individual Markdown files for human editing and refinement
|
||||||
|
- in-memory execution state treated as ephemeral
|
||||||
|
|
||||||
|
## Transcription Prompt Asset Policy
|
||||||
|
|
||||||
|
The production system treats transcription prompts as maintainable content assets.
|
||||||
|
|
||||||
|
- each transcription prompt is stored in its own Markdown file
|
||||||
|
- prompt files are designed for direct human editing and iterative refinement
|
||||||
|
- prompt updates are independent and do not require bundling unrelated prompt changes
|
||||||
|
- prompt file identity and revision history are tracked through normal repository version control
|
||||||
|
|
||||||
|
## Simplicity Guardrails
|
||||||
|
|
||||||
|
The production system enforces these constraints to prevent accidental over-engineering:
|
||||||
|
|
||||||
|
- PostgreSQL in a container is treated as a lightweight default dependency
|
||||||
|
- MongoDB in a container is treated as a lightweight optional dependency
|
||||||
|
- three containers (app, PostgreSQL, MongoDB) is an acceptable simple deployment
|
||||||
|
- no dedicated queue or search cluster is introduced without measured need
|
||||||
|
- external infrastructure is added only behind existing ports/adapters
|
||||||
|
|
||||||
|
## Extension Path
|
||||||
|
|
||||||
|
The architecture supports additive growth without changing domain contracts.
|
||||||
|
|
||||||
|
### Stage 1: Foundation (Current)
|
||||||
|
|
||||||
|
- upload, transcription, review, search, export
|
||||||
|
- in-process worker execution
|
||||||
|
- single provider adapter
|
||||||
|
- app plus PostgreSQL deployment
|
||||||
|
|
||||||
|
### Stage 2: Throughput Hardening
|
||||||
|
|
||||||
|
- optional MongoDB document-store enablement
|
||||||
|
- optional external worker/queue process
|
||||||
|
- stronger retry and dead-letter handling
|
||||||
|
|
||||||
|
### Stage 3: Intelligence Features
|
||||||
|
|
||||||
|
- entity extraction and cross-document linking
|
||||||
|
- timeline and narrative assembly
|
||||||
|
- optional multi-provider routing
|
||||||
|
|
||||||
|
Each stage preserves existing module boundaries and keeps migration risk low.
|
||||||
|
|
||||||
|
## Test Strategy
|
||||||
|
|
||||||
|
The test strategy is aligned to personal-scale operation with fast, deterministic feedback.
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
- domain transcription rules and annotation behavior
|
||||||
|
- revision-history invariants
|
||||||
|
- job state-transition logic
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
- repository behavior and transaction boundaries
|
||||||
|
- persistence-adapter and provider adapter contract mapping
|
||||||
|
- upload-to-persistence roundtrip
|
||||||
|
|
||||||
|
### End-to-End Tests
|
||||||
|
|
||||||
|
- happy path: upload, transcribe, review, search, export
|
||||||
|
- failure path: provider error, retry, surfaced failed status
|
||||||
|
|
||||||
|
### CI Execution Model
|
||||||
|
|
||||||
|
- fast suite on each push
|
||||||
|
- optional slower provider-sandbox checks on scheduled runs
|
||||||
|
|
||||||
|
## Risks And Controls
|
||||||
|
|
||||||
|
### Runtime Responsiveness
|
||||||
|
|
||||||
|
Risk:
|
||||||
|
|
||||||
|
- long jobs can reduce responsiveness in a single-process deployment
|
||||||
|
|
||||||
|
Control:
|
||||||
|
|
||||||
|
- bounded concurrency and visible job status in the UI
|
||||||
|
|
||||||
|
### Database Concurrency Limits
|
||||||
|
|
||||||
|
Risk:
|
||||||
|
|
||||||
|
- contention can appear under sustained concurrent writes in personal-scale infrastructure
|
||||||
|
|
||||||
|
Control:
|
||||||
|
|
||||||
|
- tuned connection pooling and phased use of MongoDB for document-heavy workloads
|
||||||
|
|
||||||
|
### Provider Output Variance
|
||||||
|
|
||||||
|
Risk:
|
||||||
|
|
||||||
|
- transcription quality varies by content source type, handwriting legibility, and source quality
|
||||||
|
|
||||||
|
Control:
|
||||||
|
|
||||||
|
- first-class human review and immutable revision history
|
||||||
|
|
||||||
|
## Technology References
|
||||||
|
|
||||||
|
- [FastAPI documentation](https://fastapi.tiangolo.com/)
|
||||||
|
- [NiceGUI documentation](https://nicegui.io/documentation)
|
||||||
|
- [Docker Compose documentation](https://docs.docker.com/compose/)
|
||||||
|
- [PostgreSQL documentation](https://www.postgresql.org/docs/)
|
||||||
|
- [MongoDB documentation](https://www.mongodb.com/docs/)
|
||||||
|
|
||||||
|
## Related Local References
|
||||||
|
|
||||||
|
- [System overview](index.md)
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- Adapter: A component that translates between internal interfaces and external systems such as databases or AI services.
|
||||||
|
- Background job: Work executed outside the request/response path so the UI remains responsive.
|
||||||
|
- Boundary: A strict separation between modules with different responsibilities.
|
||||||
|
- CI (Continuous Integration): Automated test execution for code changes.
|
||||||
|
- Contract test: A test that verifies an adapter follows expected input/output behavior at a boundary.
|
||||||
|
- Domain layer: The module that contains core business rules and invariants.
|
||||||
|
- End-to-end test: A test that validates a full user flow across the running system.
|
||||||
|
- Full-text search: Text indexing and querying optimized for natural-language search.
|
||||||
|
- In-process worker: A background executor that runs within the same application process.
|
||||||
|
- Integration test: A test that verifies interactions between real modules and infrastructure components.
|
||||||
|
- MongoDB: A document-oriented database used for flexible, high-variance data structures.
|
||||||
|
- Modular monolith: A single deployable application with strongly separated internal modules.
|
||||||
|
- Port/Interface: A stable contract used by application/domain code to call infrastructure implementations.
|
||||||
|
- Prompt artifact: A single Markdown file that defines one transcription prompt and can be revised independently.
|
||||||
|
- Provenance: Metadata that records where generated data came from and how it was produced.
|
||||||
|
- Revision history: Optional versioned record of user-authored transcription edits over time.
|
||||||
|
- System of record: The authoritative persistent store for canonical data.
|
||||||
|
- Vertical slice: A minimal end-to-end feature path spanning UI/API, application logic, and persistence.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
## Document Transcription System
|
||||||
|
|
||||||
|
This project is a production application for transcribing and preserving historical family documents. It is intentionally designed for personal-scale use, with a simplicity-first architecture that is easy to operate and easy to extend.
|
||||||
|
|
||||||
|
## Start Here
|
||||||
|
|
||||||
|
Read [architecture.md](architecture.md) first.
|
||||||
|
|
||||||
|
The architecture page is the primary technical reference and defines:
|
||||||
|
|
||||||
|
- deployed topology and infrastructure limits
|
||||||
|
- module boundaries and dependency flow
|
||||||
|
- processing life cycle and data ownership
|
||||||
|
- test strategy, risk controls, and extension path
|
||||||
|
|
||||||
|
## What The Application Does
|
||||||
|
|
||||||
|
At a high level, users upload images or PDFs as content sources for handwritten, typed, or typeset documents, run asynchronous transcription jobs, review optional revisions, and search across accepted text.
|
||||||
|
|
||||||
|
Core capabilities:
|
||||||
|
|
||||||
|
- document grouping with one or more content sources and metadata capture
|
||||||
|
- asynchronous transcription with visible job status
|
||||||
|
- immutable original transcription persisted with each job (plus provider/model/prompt metadata)
|
||||||
|
- transcription prompt management with one Markdown file per prompt for human refinement over time
|
||||||
|
- optional revisions for user-authored edits of original immutable transcription text
|
||||||
|
- full-text search over accepted transcripts
|
||||||
|
- export of transcript data
|
||||||
|
|
||||||
|
## Production Operating Model
|
||||||
|
|
||||||
|
The system runs with minimal operational overhead:
|
||||||
|
|
||||||
|
- PostgreSQL in a dedicated Docker container is considered extremely lightweight and simple for this system
|
||||||
|
- MongoDB in a dedicated Docker container is also considered extremely lightweight and simple for document-centric persistence
|
||||||
|
- a three-container deployment (app, PostgreSQL, MongoDB) is a simple and acceptable baseline
|
||||||
|
- no required queue or search-engine containers in the baseline setup
|
||||||
|
|
||||||
|
This operating model keeps deployment and maintenance simple while preserving clean boundaries for future scale.
|
||||||
|
|
||||||
|
## Documentation Map
|
||||||
|
|
||||||
|
- Architecture and technical design: [architecture.md](architecture.md)
|
||||||
|
- Runtime and deployment requirements: [requirements.md](requirements.md)
|
||||||
|
- Error handling policy and operational guidance: [error_handling.md](error_handling.md)
|
||||||
|
- Domain context and transcription policy: [intent.md](intent.md)
|
||||||
|
- Transcription Methodology: [transcription_methodology.md](transcription_methodology.md)
|
||||||
|
- Data model: [schema.md](schema.md)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- Document-oriented persistence: Storing data as flexible records instead of fixed relational rows.
|
||||||
|
- Prompt artifact: A single Markdown file that defines one transcription prompt and is edited independently.
|
||||||
|
- System of record: The authoritative persistent store for canonical data.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
## Document Transcription System Requirements
|
||||||
|
|
||||||
|
This page captures a SysML v1.6-style requirements baseline for the production system described in [index.md](index.md). The model is represented as concise tables and traceability lists that preserve SysML-style IDs and relationship semantics.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- System of interest: the single Python application service (NiceGUI + FastAPI) with PostgreSQL as the relational system of record and optional MongoDB for document-oriented persistence.
|
||||||
|
- Operational context: local-first execution with Docker Compose and an intentionally lightweight production trajectory.
|
||||||
|
- Primary concern: end-to-end transcription job lifecycle from upload through completion or failure.
|
||||||
|
|
||||||
|
## Requirements Model (Concise Text Form)
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
| ID | Category | Requirement | Risk | Verify Method |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| REQ-0 | System | Provide end-to-end document transcription with persistent, inspectable lifecycle state. | medium | demonstration |
|
||||||
|
| REQ-1 | Functional | Allow users to upload one or more images or PDFs as sources from the web UI. | low | test |
|
||||||
|
| REQ-2 | Functional | Run each upload through asynchronous processing that returns an original transcription or explicit failure. | high | test |
|
||||||
|
| REQ-3 | Functional | Persist and expose job states: queued, processing, transcribed, failed. | high | inspection |
|
||||||
|
| REQ-4 | Functional | Persist transcription output, processing history, and failure details. | medium | test |
|
||||||
|
| REQ-5 | Interface | Expose API and UI views for status inspection and completed transcription reading. | medium | demonstration |
|
||||||
|
| REQ-6 | Performance | Trigger background processing on upload to preserve UI responsiveness. | medium | analysis |
|
||||||
|
| REQ-7 | Design Constraint | Keep lifespan-owned runtime resources: SQLAlchemy engine, async session factory, worker resources, provider clients. | medium | inspection |
|
||||||
|
| REQ-8 | Design Constraint | Initialize configuration and logging once at startup through centralized mechanisms. | low | inspection |
|
||||||
|
| REQ-9 | Design Constraint | Use Docker Compose baseline of app plus PostgreSQL; allow optional MongoDB container when enabled. | medium | demonstration |
|
||||||
|
| REQ-10 | Design Constraint | Keep schema bootstrap explicit and opt-in; normal startup does not mutate production schema. | high | inspection |
|
||||||
|
| REQ-11 | Design Constraint | Use service-backed persistence for core document and job data. | medium | inspection |
|
||||||
|
| REQ-12 | Design Constraint | Store transcription prompts as individual Markdown artifacts for iterative refinement. | medium | inspection |
|
||||||
|
| REQ-13 | Functional | Allow users to create one optional revision of transcription text derived from the original job transcription. | low | test |
|
||||||
|
|
||||||
|
### Requirement Relationships
|
||||||
|
|
||||||
|
- Contains: REQ-0 contains REQ-1 through REQ-13.
|
||||||
|
- Derives: REQ-2 -> REQ-3, REQ-3 -> REQ-4.
|
||||||
|
- Traces: REQ-5 -> REQ-3.
|
||||||
|
- Refines: REQ-6 -> REQ-2.
|
||||||
|
|
||||||
|
### Architecture Elements
|
||||||
|
|
||||||
|
| Element | Type | Doc Reference |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| UI | NiceGUI pages | src/transcription/ui/pages |
|
||||||
|
| API | FastAPI routes | src/transcription/api/routes.py |
|
||||||
|
| GRAPH | Async processing workflow | src/transcription/services, src/transcription/ai |
|
||||||
|
| DBREL | PostgreSQL + SQLModel relational persistence | src/transcription/db |
|
||||||
|
| DBDOC | MongoDB document persistence | src/transcription/db, src/transcription/services |
|
||||||
|
| OPS | Docker Compose runtime | docker-compose.yml |
|
||||||
|
| PROMPTS | Transcription prompt artifact library (Markdown files) | .github/prompts, docs |
|
||||||
|
| TESTS | Pytest verification suite | tests |
|
||||||
|
|
||||||
|
### Satisfaction Mapping
|
||||||
|
|
||||||
|
- UI satisfies REQ-1, REQ-5, REQ-13.
|
||||||
|
- API satisfies REQ-5.
|
||||||
|
- GRAPH satisfies REQ-2, REQ-6.
|
||||||
|
- DBREL satisfies REQ-3, REQ-10, REQ-13.
|
||||||
|
- DBDOC satisfies REQ-4, REQ-11.
|
||||||
|
- OPS satisfies REQ-9.
|
||||||
|
- PROMPTS satisfies REQ-12.
|
||||||
|
|
||||||
|
### Verification Mapping
|
||||||
|
|
||||||
|
- TESTS verifies REQ-1, REQ-2, REQ-3, REQ-4, REQ-5, REQ-10, REQ-11, REQ-12, REQ-13.
|
||||||
|
|
||||||
|
## Requirement Notes
|
||||||
|
|
||||||
|
- Requirement IDs (`REQ-*`) are stable references for planning, implementation, and test traceability.
|
||||||
|
- The model uses compact tables and traceability lists for renderer compatibility while preserving SysML-style requirement IDs and relationship semantics.
|
||||||
|
- Requirement categories (functional, interface, performance, and design constraints) are preserved as explicit REQ entries and relationship labels to keep change impact visible.
|
||||||
|
- PostgreSQL containerization and optional MongoDB containerization are both treated as extremely lightweight and simple operational choices in this architecture.
|
||||||
|
|
||||||
|
## Verification Intent
|
||||||
|
|
||||||
|
- Demonstration: validate end-to-end behavior via running system flows and operator-visible outcomes.
|
||||||
|
- Inspection: verify architecture and startup/runtime policies in code and configuration.
|
||||||
|
- Analysis: evaluate asynchronous execution behavior and design sufficiency.
|
||||||
|
- Test: automate behavioral checks through pytest suites and service-level tests.
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- Document-oriented persistence: A storage approach that uses flexible document structures for variable data shapes.
|
||||||
|
- Prompt artifact: A single Markdown file that defines one transcription prompt and is revised independently.
|
||||||
|
- SysML: Systems Modeling Language used to express structured requirements and traceability.
|
||||||
|
- System of record: The authoritative persistent store for canonical business data.
|
||||||
@@ -271,7 +271,7 @@ Change requirements:
|
|||||||
- [System overview](index.md)
|
- [System overview](index.md)
|
||||||
- [Architecture](architecture.md)
|
- [Architecture](architecture.md)
|
||||||
- [Requirements](requirements.md)
|
- [Requirements](requirements.md)
|
||||||
- [Intent](Intent.md)
|
- [Intent](intent.md)
|
||||||
|
|
||||||
## Glossary
|
## Glossary
|
||||||
|
|
||||||
|
|||||||
+32
-31
@@ -1,56 +1,57 @@
|
|||||||
## Document Transcription System
|
## Document Transcription System (V1)
|
||||||
|
|
||||||
This project is a production application for transcribing and preserving historical family documents. It is intentionally designed for personal-scale use, with a simplicity-first architecture that is easy to operate and easy to extend.
|
This project is a personal-scale application for transcribing and preserving historical family documents.
|
||||||
|
|
||||||
## Start Here
|
## Start Here
|
||||||
|
|
||||||
Read [architecture.md](architecture.md) first.
|
Read [architecture.md](architecture.md) first.
|
||||||
|
|
||||||
The architecture page is the primary technical reference and defines:
|
The architecture page is the primary technical reference for:
|
||||||
|
|
||||||
- deployed topology and infrastructure limits
|
- runtime topology and infrastructure assumptions
|
||||||
- module boundaries and dependency flow
|
- module boundaries and dependency flow
|
||||||
- processing life cycle and data ownership
|
- processing lifecycle and data ownership
|
||||||
- test strategy, risk controls, and extension path
|
- test strategy and extension path
|
||||||
|
|
||||||
## What The Application Does
|
## What The Application Does
|
||||||
|
|
||||||
At a high level, users upload images or PDFs as content sources for handwritten, typed, or typeset documents, run asynchronous transcription jobs, review optional revisions, and search across accepted text.
|
At a high level, users upload images/PDFs, jobs are processed asynchronously, and users review original transcriptions plus optional revisions.
|
||||||
|
|
||||||
Core capabilities:
|
Core V1 capabilities:
|
||||||
|
|
||||||
- document grouping with one or more content sources and metadata capture
|
- upload supported source files (`.jpg`, `.jpeg`, `.png`, `.tif`, `.tiff`, `.pdf`)
|
||||||
- asynchronous transcription with visible job status
|
- asynchronous job processing with visible status (`queued`, `processing`, `transcribed`, `failed`)
|
||||||
- immutable original transcription persisted with each job (plus provider/model/prompt metadata)
|
- immutable original transcription stored on `Job.text`
|
||||||
- transcription prompt management with one Markdown file per prompt for human refinement over time
|
- optional single user-authored revision per source (`0..1`)
|
||||||
- optional revisions for user-authored edits of original immutable transcription text
|
- prompt artifacts stored as Markdown files in `prompts/`
|
||||||
- full-text search over accepted transcripts
|
|
||||||
- export of transcript data
|
|
||||||
|
|
||||||
## Production Operating Model
|
## Current Operating Model (V1 Baseline)
|
||||||
|
|
||||||
The system runs with minimal operational overhead:
|
- application service: FastAPI + NiceGUI
|
||||||
|
- persistence baseline: SQLModel with SQLite
|
||||||
|
- worker: in-process async background loop
|
||||||
|
- deployment baseline: lightweight Docker Compose app runtime
|
||||||
|
|
||||||
- PostgreSQL in a dedicated Docker container is considered extremely lightweight and simple for this system
|
> Planned persistence evolution (PostgreSQL and optional MongoDB) belongs to V2 planning and is tracked separately.
|
||||||
- MongoDB in a dedicated Docker container is also considered extremely lightweight and simple for document-centric persistence
|
|
||||||
- a three-container deployment (app, PostgreSQL, MongoDB) is a simple and acceptable baseline
|
|
||||||
- no required queue or search-engine containers in the baseline setup
|
|
||||||
|
|
||||||
This operating model keeps deployment and maintenance simple while preserving clean boundaries for future scale.
|
|
||||||
|
|
||||||
## Documentation Map
|
## Documentation Map
|
||||||
|
|
||||||
- Architecture and technical design: [architecture.md](architecture.md)
|
- Architecture and technical design: [architecture.md](architecture.md)
|
||||||
- Runtime and deployment requirements: [requirements.md](requirements.md)
|
- V1 runtime and requirement baseline: [requirements.md](requirements.md)
|
||||||
|
- Data model and constraints: [schema.md](schema.md)
|
||||||
- Error handling policy and operational guidance: [error_handling.md](error_handling.md)
|
- Error handling policy and operational guidance: [error_handling.md](error_handling.md)
|
||||||
|
- V1 requirement evidence matrix: [traceability_v1.md](traceability_v1.md)
|
||||||
|
- Operations runbook: [runbook.md](runbook.md)
|
||||||
|
- V1 migration and rollback guidance: [migration_v1.md](migration_v1.md)
|
||||||
|
- V1 release checklist: [release_checklist_v1.md](release_checklist_v1.md)
|
||||||
- Domain context and transcription policy: [intent.md](intent.md)
|
- Domain context and transcription policy: [intent.md](intent.md)
|
||||||
- Transcription Methodology: [transcription_methodology.md](transcription_methodology.md)
|
- Transcription methodology: [transcription_methodology.md](transcription_methodology.md)
|
||||||
- Data model: [schema.md](schema.md)
|
- V1 execution plan: [ver1/ver1.md](ver1/ver1.md)
|
||||||
|
- V2 roadmap: [ver2/ver2.md](ver2/ver2.md)
|
||||||
|
|
||||||
|
|
||||||
## Glossary
|
## Glossary
|
||||||
|
|
||||||
- Document-oriented persistence: Storing data as flexible records instead of fixed relational rows.
|
- Prompt artifact: a Markdown file containing one transcription prompt.
|
||||||
- Prompt artifact: A single Markdown file that defines one transcription prompt and is edited independently.
|
- Original transcription: immutable provider output stored on `Job.text`.
|
||||||
- System of record: The authoritative persistent store for canonical data.
|
- Revision: optional user-authored text linked to a `Source`.
|
||||||
|
- System of record: the authoritative persistent store for canonical application data.
|
||||||
|
|||||||
+37
-36
@@ -1,12 +1,14 @@
|
|||||||
## Document Transcription System Requirements
|
## Document Transcription System Requirements (V1 Baseline)
|
||||||
|
|
||||||
This page captures a SysML v1.6-style requirements baseline for the production system described in [index.md](index.md). The model is represented as concise tables and traceability lists that preserve SysML-style IDs and relationship semantics.
|
This page captures the **Version 1 baseline requirements** for the currently implemented system. It is the source of truth for V1 acceptance and test traceability.
|
||||||
|
|
||||||
|
Forward-looking architecture changes (for example PostgreSQL/Mongo adoption) are intentionally out of this document and should be tracked in a V2 planning/backlog artifact.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
- System of interest: the single Python application service (NiceGUI + FastAPI) with PostgreSQL as the relational system of record and optional MongoDB for document-oriented persistence.
|
- System of interest: a single Python application service (NiceGUI + FastAPI) with SQLModel persistence.
|
||||||
- Operational context: local-first execution with Docker Compose and an intentionally lightweight production trajectory.
|
- Runtime/persistence baseline: local-first execution using SQLite (default `sqlite:///./transcription.db`), with Docker Compose support.
|
||||||
- Primary concern: end-to-end transcription job lifecycle from upload through completion or failure.
|
- Primary concern: end-to-end transcription lifecycle from upload through terminal state plus optional single revision editing.
|
||||||
|
|
||||||
## Requirements Model (Concise Text Form)
|
## Requirements Model (Concise Text Form)
|
||||||
|
|
||||||
@@ -15,19 +17,19 @@ This page captures a SysML v1.6-style requirements baseline for the production s
|
|||||||
| ID | Category | Requirement | Risk | Verify Method |
|
| ID | Category | Requirement | Risk | Verify Method |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| REQ-0 | System | Provide end-to-end document transcription with persistent, inspectable lifecycle state. | medium | demonstration |
|
| REQ-0 | System | Provide end-to-end document transcription with persistent, inspectable lifecycle state. | medium | demonstration |
|
||||||
| REQ-1 | Functional | Allow users to upload one or more images or PDFs as sources from the web UI. | low | test |
|
| REQ-1 | Functional | Allow users to upload supported image/PDF files as sources from the web UI. | low | test |
|
||||||
| REQ-2 | Functional | Run each upload through asynchronous processing that returns an original transcription or explicit failure. | high | test |
|
| REQ-2 | Functional | Process uploads asynchronously and return either original transcription output or explicit failure. | high | test |
|
||||||
| REQ-3 | Functional | Persist and expose job states: queued, processing, transcribed, failed. | high | inspection |
|
| REQ-3 | Functional | Persist and expose job states: `queued`, `processing`, `transcribed`, `failed`. | high | inspection |
|
||||||
| REQ-4 | Functional | Persist transcription output, processing history, and failure details. | medium | test |
|
| REQ-4 | Functional | Persist original provider output (`Job.text`) and failure detail (`Job.error_detail`) for each job. | medium | test |
|
||||||
| REQ-5 | Interface | Expose API and UI views for status inspection and completed transcription reading. | medium | demonstration |
|
| REQ-5 | Interface | Expose API/UI views for status inspection and transcription reading. | medium | demonstration |
|
||||||
| REQ-6 | Performance | Trigger background processing on upload to preserve UI responsiveness. | medium | analysis |
|
| REQ-6 | Performance | Trigger background processing on upload to preserve UI responsiveness. | medium | analysis |
|
||||||
| REQ-7 | Design Constraint | Keep lifespan-owned runtime resources: SQLAlchemy engine, async session factory, worker resources, provider clients. | medium | inspection |
|
| REQ-7 | Design Constraint | Keep lifespan-owned runtime resources (engine/session factory/worker resources) initialized and disposed at application boundaries. | medium | inspection |
|
||||||
| REQ-8 | Design Constraint | Initialize configuration and logging once at startup through centralized mechanisms. | low | inspection |
|
| REQ-8 | Design Constraint | Initialize configuration and logging once at startup through centralized mechanisms. | low | inspection |
|
||||||
| REQ-9 | Design Constraint | Use Docker Compose baseline of app plus PostgreSQL; allow optional MongoDB container when enabled. | medium | demonstration |
|
| REQ-9 | Design Constraint | Support containerized app runtime via Docker Compose using the same V1 persistence model. | medium | demonstration |
|
||||||
| REQ-10 | Design Constraint | Keep schema bootstrap explicit and opt-in; normal startup does not mutate production schema. | high | inspection |
|
| REQ-10 | Design Constraint | Keep schema bootstrap explicit and opt-in for production safety. | high | inspection |
|
||||||
| REQ-11 | Design Constraint | Use service-backed persistence for core document and job data. | medium | inspection |
|
| REQ-11 | Design Constraint | Route persistence changes through service/workflow orchestration boundaries. | medium | inspection |
|
||||||
| REQ-12 | Design Constraint | Store transcription prompts as individual Markdown artifacts for iterative refinement. | medium | inspection |
|
| REQ-12 | Design Constraint | Store transcription prompts as individual Markdown artifacts for iterative refinement. | medium | inspection |
|
||||||
| REQ-13 | Functional | Allow users to create one optional revision of transcription text derived from the original job transcription. | low | test |
|
| REQ-13 | Functional | Allow users to create/update one optional revision derived from the original job transcription and view/delete it from the job detail flow. | low | test |
|
||||||
|
|
||||||
### Requirement Relationships
|
### Requirement Relationships
|
||||||
|
|
||||||
@@ -40,22 +42,22 @@ This page captures a SysML v1.6-style requirements baseline for the production s
|
|||||||
|
|
||||||
| Element | Type | Doc Reference |
|
| Element | Type | Doc Reference |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| UI | NiceGUI pages | src/transcription/ui/pages |
|
| UI | NiceGUI pages/components | `src/transcription/ui/pages`, `src/transcription/ui/components` |
|
||||||
| API | FastAPI routes | src/transcription/api/routes.py |
|
| API | FastAPI routes and handlers | `src/transcription/api`, `src/transcription/app.py` |
|
||||||
| GRAPH | Async processing workflow | src/transcription/services, src/transcription/ai |
|
| WORKER | Async queued-job processing workflow | `src/transcription/worker.py`, `src/transcription/services/workflows.py` |
|
||||||
| DBREL | PostgreSQL + SQLModel relational persistence | src/transcription/db |
|
| DBREL | SQLModel relational persistence (SQLite in V1 baseline) | `src/transcription/models.py`, `src/transcription/db` |
|
||||||
| DBDOC | MongoDB document persistence | src/transcription/db, src/transcription/services |
|
| SERVICES | Service-layer persistence orchestration | `src/transcription/services` |
|
||||||
| OPS | Docker Compose runtime | docker-compose.yml |
|
| OPS | Containerized runtime baseline | `docker-compose.yml`, `Dockerfile` |
|
||||||
| PROMPTS | Transcription prompt artifact library (Markdown files) | .github/prompts, docs |
|
| PROMPTS | Transcription prompt artifacts | `prompts/` |
|
||||||
| TESTS | Pytest verification suite | tests |
|
| TESTS | Pytest verification suite | `tests/` |
|
||||||
|
|
||||||
### Satisfaction Mapping
|
### Satisfaction Mapping
|
||||||
|
|
||||||
- UI satisfies REQ-1, REQ-5, REQ-13.
|
- UI satisfies REQ-1, REQ-5, REQ-13.
|
||||||
- API satisfies REQ-5.
|
- API satisfies REQ-5.
|
||||||
- GRAPH satisfies REQ-2, REQ-6.
|
- WORKER satisfies REQ-2, REQ-6.
|
||||||
- DBREL satisfies REQ-3, REQ-10, REQ-13.
|
- DBREL satisfies REQ-3, REQ-4, REQ-10, REQ-13.
|
||||||
- DBDOC satisfies REQ-4, REQ-11.
|
- SERVICES satisfies REQ-4, REQ-11.
|
||||||
- OPS satisfies REQ-9.
|
- OPS satisfies REQ-9.
|
||||||
- PROMPTS satisfies REQ-12.
|
- PROMPTS satisfies REQ-12.
|
||||||
|
|
||||||
@@ -65,21 +67,20 @@ This page captures a SysML v1.6-style requirements baseline for the production s
|
|||||||
|
|
||||||
## Requirement Notes
|
## Requirement Notes
|
||||||
|
|
||||||
- Requirement IDs (`REQ-*`) are stable references for planning, implementation, and test traceability.
|
- Requirement IDs (`REQ-*`) are stable references for planning, implementation, and traceability.
|
||||||
- The model uses compact tables and traceability lists for renderer compatibility while preserving SysML-style requirement IDs and relationship semantics.
|
- This document is intentionally **implementation-aligned** for V1 completion and release sign-off.
|
||||||
- Requirement categories (functional, interface, performance, and design constraints) are preserved as explicit REQ entries and relationship labels to keep change impact visible.
|
- Planned storage evolution (PostgreSQL and optional MongoDB) is a **V2 concern** and should be tracked outside this V1 baseline.
|
||||||
- PostgreSQL containerization and optional MongoDB containerization are both treated as extremely lightweight and simple operational choices in this architecture.
|
|
||||||
|
|
||||||
## Verification Intent
|
## Verification Intent
|
||||||
|
|
||||||
- Demonstration: validate end-to-end behavior via running system flows and operator-visible outcomes.
|
- Demonstration: validate end-to-end behavior through operator-visible flows.
|
||||||
- Inspection: verify architecture and startup/runtime policies in code and configuration.
|
- Inspection: verify architecture and startup/runtime policies in code and configuration.
|
||||||
- Analysis: evaluate asynchronous execution behavior and design sufficiency.
|
- Analysis: evaluate asynchronous execution behavior and design sufficiency.
|
||||||
- Test: automate behavioral checks through pytest suites and service-level tests.
|
- Test: automate behavioral checks through pytest suites and service/UI integration tests.
|
||||||
|
|
||||||
## Glossary
|
## Glossary
|
||||||
|
|
||||||
- Document-oriented persistence: A storage approach that uses flexible document structures for variable data shapes.
|
- Original transcription: immutable provider output stored on `Job.text`.
|
||||||
- Prompt artifact: A single Markdown file that defines one transcription prompt and is revised independently.
|
- Revision: optional user-authored editable text tied to a `Source` (`0..1` in V1).
|
||||||
- SysML: Systems Modeling Language used to express structured requirements and traceability.
|
- Prompt artifact: a Markdown file containing instructions used for transcription.
|
||||||
- System of record: The authoritative persistent store for canonical business data.
|
- System of record: the authoritative relational store for canonical V1 data.
|
||||||
|
|||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
# V1 Operations Runbook
|
||||||
|
|
||||||
|
This runbook provides day-2 operational procedures for the V1 baseline.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Applies to:
|
||||||
|
|
||||||
|
- local/hosted V1 runtime
|
||||||
|
- SQLite-backed persistence
|
||||||
|
- in-process worker lifecycle
|
||||||
|
- OpenRouter provider integration
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
- `.env` contains `OPENROUTER_API_KEY`
|
||||||
|
- app starts successfully
|
||||||
|
- `uploads/` and `prompts/` are writable
|
||||||
|
- health endpoint responds at `/healthz`
|
||||||
|
|
||||||
|
## Standard Startup Procedure
|
||||||
|
|
||||||
|
1. Start the app using the project-standard command.
|
||||||
|
2. Open `/healthz` and verify `{"status":"ok"}`.
|
||||||
|
3. Open `/ui/upload` and submit a small valid file.
|
||||||
|
4. Confirm job transitions from `queued` -> `processing` -> `transcribed` (or `failed` with detail).
|
||||||
|
|
||||||
|
## Standard Shutdown Procedure
|
||||||
|
|
||||||
|
1. Stop the application process.
|
||||||
|
2. Ensure no active process still holds the SQLite file.
|
||||||
|
3. If maintenance is planned, copy the DB file before edits:
|
||||||
|
- `transcription.db` (or configured `DATABASE_URL` file path)
|
||||||
|
|
||||||
|
## Incident: Jobs Stuck In `processing`
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
|
||||||
|
- Jobs remain `processing` for longer than provider timeout
|
||||||
|
- New uploads queue but do not complete
|
||||||
|
- provider usage increases but no terminal job state is visible
|
||||||
|
|
||||||
|
### Checks
|
||||||
|
|
||||||
|
1. Confirm app process is still running.
|
||||||
|
2. Confirm worker loop is active (startup logs include worker lifespan start).
|
||||||
|
3. Inspect recent app logs for:
|
||||||
|
- `worker.process_job`
|
||||||
|
- `error_id`
|
||||||
|
- `category`
|
||||||
|
- `job_id` / `document_id` / `source_id`
|
||||||
|
4. Verify provider credentials and provider status.
|
||||||
|
|
||||||
|
### Recovery
|
||||||
|
|
||||||
|
1. Restart the app to trigger stale-processing recovery.
|
||||||
|
2. On startup, app re-queues stale processing jobs based on timeout policy.
|
||||||
|
3. Re-check jobs page and confirm terminal state progression.
|
||||||
|
4. If persistent, capture logs + error IDs and move to deep investigation.
|
||||||
|
|
||||||
|
## Incident: Provider Authentication Failures
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
|
||||||
|
- failures categorized as provider/auth
|
||||||
|
- jobs fail quickly with authentication guidance
|
||||||
|
|
||||||
|
### Recovery
|
||||||
|
|
||||||
|
1. Validate `OPENROUTER_API_KEY` value.
|
||||||
|
2. Restart app after updating env.
|
||||||
|
3. Re-run a small transcription to confirm recovery.
|
||||||
|
|
||||||
|
## Incident: Upload Failures
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
|
||||||
|
- UI reports upload errors
|
||||||
|
- unsupported extension or empty payload
|
||||||
|
|
||||||
|
### Recovery
|
||||||
|
|
||||||
|
1. Validate file extension (`.jpg`, `.jpeg`, `.png`, `.tif`, `.tiff`, `.pdf`).
|
||||||
|
2. Validate file is not empty.
|
||||||
|
3. Validate upload directory permissions.
|
||||||
|
4. Retry upload.
|
||||||
|
|
||||||
|
## Incident: Database File/Permission Issues
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
|
||||||
|
- persistence errors during upload/job update
|
||||||
|
- startup failures around schema/runtime
|
||||||
|
|
||||||
|
### Recovery
|
||||||
|
|
||||||
|
1. Confirm the configured DB file path exists and is writable.
|
||||||
|
2. Confirm parent directory permissions.
|
||||||
|
3. Restore from last known backup copy if corruption is suspected.
|
||||||
|
4. Restart app and run smoke test.
|
||||||
|
|
||||||
|
## Logging Requirements (Operational)
|
||||||
|
|
||||||
|
Operational triage should always capture:
|
||||||
|
|
||||||
|
- `error_id`
|
||||||
|
- category
|
||||||
|
- operation name
|
||||||
|
- `job_id`, `document_id`, `source_id` when applicable
|
||||||
|
- UTC timestamp
|
||||||
|
|
||||||
|
## Escalation Packet (When opening an issue)
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
- exact timestamp window
|
||||||
|
- one failing `job_id`
|
||||||
|
- relevant `error_id` values
|
||||||
|
- latest 100 lines of app logs
|
||||||
|
- environment summary (`DATABASE_URL` type, app version/commit)
|
||||||
|
|
||||||
|
## Post-Incident Validation
|
||||||
|
|
||||||
|
After mitigation, verify:
|
||||||
|
|
||||||
|
1. Upload works.
|
||||||
|
2. One job reaches `transcribed`.
|
||||||
|
3. One induced failure reaches `failed` with error detail.
|
||||||
|
4. Jobs page and detail page render correctly.
|
||||||
+47
-34
@@ -1,21 +1,23 @@
|
|||||||
|
## Database Schema (V1 Baseline)
|
||||||
|
|
||||||
## Database schema
|
This document describes the current relational schema for the transcription system.
|
||||||
This document describes the structure of the database underlying the personal historical-document transcription system.
|
|
||||||
|
All primary and foreign keys in the domain models are UUID-based in V1.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Schema diagram
|
## Schema Diagram
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
erDiagram
|
erDiagram
|
||||||
document {
|
DOCUMENT {
|
||||||
INTEGER id PK
|
UUID id PK
|
||||||
TEXT name
|
TEXT name
|
||||||
}
|
}
|
||||||
|
|
||||||
job {
|
JOB {
|
||||||
INTEGER id PK
|
UUID id PK
|
||||||
INTEGER document_id FK
|
UUID document_id FK
|
||||||
TEXT status
|
TEXT status
|
||||||
INTEGER retry_count
|
INTEGER retry_count
|
||||||
DATETIME date_created
|
DATETIME date_created
|
||||||
@@ -27,48 +29,59 @@ erDiagram
|
|||||||
TEXT error_detail
|
TEXT error_detail
|
||||||
}
|
}
|
||||||
|
|
||||||
source {
|
SOURCE {
|
||||||
INTEGER id PK
|
UUID id PK
|
||||||
INTEGER document_id FK
|
UUID document_id FK
|
||||||
INTEGER job_id FK
|
UUID job_id FK
|
||||||
TEXT upload_name
|
TEXT upload_name
|
||||||
TEXT filename
|
TEXT filename
|
||||||
TEXT file_path
|
TEXT file_path
|
||||||
DATETIME date_uploaded
|
DATETIME date_uploaded
|
||||||
}
|
}
|
||||||
|
|
||||||
revision {
|
REVISION {
|
||||||
INTEGER id PK
|
UUID id PK
|
||||||
INTEGER source_id FK
|
UUID source_id FK UNIQUE
|
||||||
INTEGER revision
|
INTEGER revision
|
||||||
TEXT text
|
TEXT text
|
||||||
DATETIME date_created
|
DATETIME date_created
|
||||||
}
|
}
|
||||||
|
|
||||||
document ||--o{ source : "has 0 or more"
|
DOCUMENT ||--o{ SOURCE : has_many
|
||||||
document ||--o{ job : "has 0 or more"
|
DOCUMENT ||--o{ JOB : has_many
|
||||||
job ||--o{ source : "processes 0 or more"
|
JOB ||--o{ SOURCE : referenced_by
|
||||||
source ||--o{ revision : "has 0 or 1"
|
SOURCE ||--o| REVISION : has_optional_one
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table Relationships & Constraints
|
## Table Relationships and Constraints
|
||||||
* A document can consist of 0 or more content sources. A document can have 0 or more jobs.
|
|
||||||
* A source can belong to only one job (which contains the original transcription). A source can only belong to one document. A source may have one optional transcription revision.
|
- A `Document` can have zero or more `Source` records.
|
||||||
* A job can process one or more sources. A job can belong to only one document.
|
- A `Document` can have zero or more `Job` records.
|
||||||
* A revision can belong to only one source. A source may have one optional revision.
|
- A `Source` belongs to exactly one `Document` and one `Job`.
|
||||||
* 1:1 optionality is enforced by uniqueness on `revision.source_id` (no revision history chain).
|
- A `Source` may have one optional `Revision`.
|
||||||
* `Job.text` stores the original immutable provider transcription.
|
- Optional `0..1` revision cardinality is enforced by uniqueness on `revision.source_id`.
|
||||||
* Revision rows are optional user-authored edits and are derived from the original transcription. Unlike jobs, revision rows can be updated.
|
|
||||||
|
### Invariants
|
||||||
|
|
||||||
|
- `Job.text` stores immutable original provider transcription output.
|
||||||
|
- `Revision` rows are optional user-authored edits derived from original transcription.
|
||||||
|
- Revisions do not overwrite original `Job.text`.
|
||||||
|
- Job status lifecycle values are: `queued`, `processing`, `transcribed`, `failed`.
|
||||||
|
|
||||||
|
### Timestamp Fields
|
||||||
|
|
||||||
|
- `Job.date_created`
|
||||||
|
- `Job.date_updated`
|
||||||
|
- `Source.date_uploaded`
|
||||||
|
- `Revision.date_created`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Glossary
|
## Glossary
|
||||||
* **Document** - Documents consist of one or more content sources and their related transcriptions.
|
|
||||||
* **Source** - A content source that is transcribed to text. It can either be an image (.jpg, .tiff, .png) or a PDF (.pdf).
|
- **Document**: logical grouping for one or more transcribed sources.
|
||||||
* **Image** - The scanned image of one page of a document.
|
- **Source**: uploaded file content (image/PDF) linked to a job.
|
||||||
* **PDF** - A PDF containing the image of one or more pages of a document.
|
- **Job**: processing record that stores lifecycle status and original output.
|
||||||
* **Job** - A processing job ingests one or more sources, sends them to an AI model along with a prompt for transcription, then stores the results. The results are immutable, *including the original transcription*. The user can create a revision of the original transcription, but the user cannot modify the original.
|
- **Revision**: optional single user-authored edited text linked to a source.
|
||||||
* **Transcription** - The text contained in a content source. A job creates the original immutable transcription. A user can optionally create a revised transcription, or "revision".
|
|
||||||
* **Revision** - A revision is a user-created modification of an existing transcription. It is optional. Some original transcriptions will have no revisions.
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# V1 Data Migration and Recovery Guidance
|
||||||
|
|
||||||
|
This document defines migration/backfill and rollback guidance for the V1 SQLite baseline.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
- provide safe procedures for local schema evolution and recovery
|
||||||
|
- reduce data-loss risk during version upgrades
|
||||||
|
- establish repeatable pre-change and post-change checks
|
||||||
|
|
||||||
|
## Current Baseline
|
||||||
|
|
||||||
|
- canonical relational store: SQLite
|
||||||
|
- default DB path: `./transcription.db`
|
||||||
|
- schema bootstrap may apply compatibility updates for dev/test scenarios
|
||||||
|
|
||||||
|
## Pre-Change Checklist
|
||||||
|
|
||||||
|
Before changing runtime version or schema behavior:
|
||||||
|
|
||||||
|
1. Stop the app process.
|
||||||
|
2. Create a timestamped DB backup copy.
|
||||||
|
3. Capture current app commit/version.
|
||||||
|
4. Export a quick status inventory:
|
||||||
|
- job counts by status
|
||||||
|
- total documents/sources/revisions
|
||||||
|
5. Ensure sufficient disk space.
|
||||||
|
|
||||||
|
## Backup Procedure (SQLite)
|
||||||
|
|
||||||
|
Minimum procedure:
|
||||||
|
|
||||||
|
1. Stop app.
|
||||||
|
2. Copy DB file to a safe location with timestamp.
|
||||||
|
3. Store backup path in release notes or change log.
|
||||||
|
|
||||||
|
## Upgrade Procedure (V1)
|
||||||
|
|
||||||
|
1. Perform pre-change checklist.
|
||||||
|
2. Deploy updated app version.
|
||||||
|
3. Start app and observe startup logs.
|
||||||
|
4. Verify schema bootstrap completes (if enabled).
|
||||||
|
5. Run smoke flow:
|
||||||
|
- upload valid file
|
||||||
|
- observe terminal status
|
||||||
|
- open job detail
|
||||||
|
|
||||||
|
## Backfill Guidance
|
||||||
|
|
||||||
|
V1 backfill is limited and conservative:
|
||||||
|
|
||||||
|
- for records missing newly introduced non-null defaults, use explicit one-time SQL updates only after backup
|
||||||
|
- avoid destructive rewrites of `Job.text` or `Revision.text`
|
||||||
|
- never backfill by overwriting original immutable transcription output
|
||||||
|
|
||||||
|
## Rollback Procedure
|
||||||
|
|
||||||
|
If upgrade fails or causes data inconsistency:
|
||||||
|
|
||||||
|
1. Stop app.
|
||||||
|
2. Restore prior DB backup file.
|
||||||
|
3. Revert app version to last known-good commit.
|
||||||
|
4. Restart app.
|
||||||
|
5. Run smoke flow and confirm stability.
|
||||||
|
|
||||||
|
## Recovery Scenarios
|
||||||
|
|
||||||
|
### Stale processing jobs after crash/restart
|
||||||
|
|
||||||
|
- restart app and allow stale-job recovery to re-queue timed-out `processing` jobs
|
||||||
|
- monitor for terminal progression
|
||||||
|
|
||||||
|
### Schema mismatch symptoms
|
||||||
|
|
||||||
|
- errors during startup or writes indicating missing columns/indexes
|
||||||
|
- rollback to last good DB + app version
|
||||||
|
- reattempt with documented upgrade path
|
||||||
|
|
||||||
|
## Validation Evidence
|
||||||
|
|
||||||
|
For each upgrade rehearsal, capture:
|
||||||
|
|
||||||
|
- backup filename/path
|
||||||
|
- pre and post job status counts
|
||||||
|
- smoke test result
|
||||||
|
- rollback rehearsal result (recommended)
|
||||||
|
|
||||||
|
## Operational Constraints
|
||||||
|
|
||||||
|
- treat DB backups as required before non-trivial upgrades
|
||||||
|
- do not perform in-place DB edits while app is running
|
||||||
|
- do not skip post-upgrade smoke validation
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# V1 Release Readiness Checklist
|
||||||
|
|
||||||
|
Use this checklist before declaring V1 operationally complete.
|
||||||
|
|
||||||
|
## A) Functional Readiness
|
||||||
|
|
||||||
|
- [ ] Upload flow works for supported file types.
|
||||||
|
- [ ] Worker transitions jobs through `queued -> processing -> transcribed|failed`.
|
||||||
|
- [ ] Job detail displays immutable original transcription from `Job.text`.
|
||||||
|
- [ ] Revision workflow supports create/update/view/delete for optional single revision.
|
||||||
|
|
||||||
|
## B) Reliability and Error Handling
|
||||||
|
|
||||||
|
- [ ] Error categories surface with actionable messages in UI/API pathways.
|
||||||
|
- [ ] Failed jobs persist `error_detail` and terminal state.
|
||||||
|
- [ ] Stale processing recovery verified on restart.
|
||||||
|
- [ ] Retry/timeout behavior validated against configured limits.
|
||||||
|
|
||||||
|
## C) Operational Readiness
|
||||||
|
|
||||||
|
- [ ] `docs/runbook.md` reviewed and current.
|
||||||
|
- [ ] `docs/migration_v1.md` reviewed and current.
|
||||||
|
- [ ] Backup and rollback procedures tested at least once.
|
||||||
|
- [ ] Incident escalation packet template is known to operators.
|
||||||
|
|
||||||
|
## D) Quality Gates
|
||||||
|
|
||||||
|
- [ ] Lint/type checks pass.
|
||||||
|
- [ ] `pytest -m "not external" -q` passes.
|
||||||
|
- [ ] Targeted external/provider checks executed (if credentials available).
|
||||||
|
- [ ] Release evidence recorded in `docs/release_evidence_v1.md`.
|
||||||
|
|
||||||
|
## E) Traceability and Documentation
|
||||||
|
|
||||||
|
- [ ] `docs/requirements.md` aligns with implemented V1 behavior.
|
||||||
|
- [ ] `docs/architecture.md`, `docs/schema.md`, and `docs/error_handling.md` are consistent.
|
||||||
|
- [ ] `docs/traceability_v1.md` is updated with current implementation and test evidence.
|
||||||
|
- [ ] `docs/ver1/ver1.md` phase status updated with evidence references.
|
||||||
|
- [ ] REQ traceability evidence links recorded (tests/runbook/checks).
|
||||||
|
|
||||||
|
## Release Sign-Off
|
||||||
|
|
||||||
|
- [ ] Technical sign-off complete.
|
||||||
|
- [ ] Operational sign-off complete.
|
||||||
|
- [ ] V1 completion date recorded.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# V1 Release Evidence Log
|
||||||
|
|
||||||
|
## Step 5 Quality Gates (2026-07-29)
|
||||||
|
|
||||||
|
### Lint
|
||||||
|
|
||||||
|
- Command: `python -m ruff check .`
|
||||||
|
- Result: ✅ pass
|
||||||
|
- Notes: initial findings were auto-fixed (`ruff --fix`) plus small manual line-wrap/annotation adjustments.
|
||||||
|
|
||||||
|
### Tests (primary gate)
|
||||||
|
|
||||||
|
- Command: `python -m pytest -m "not external" -q`
|
||||||
|
- Result: ✅ pass (`[100%]`)
|
||||||
|
|
||||||
|
### Tests (external smoke)
|
||||||
|
|
||||||
|
- Command: `python -m pytest -m external -q`
|
||||||
|
- Result: ✅ pass (`[100%]`)
|
||||||
|
|
||||||
|
### Type Check
|
||||||
|
|
||||||
|
- Command: `python -m ty check src tests`
|
||||||
|
- Result: ⚠️ not passing
|
||||||
|
- Summary: existing SQLModel/SQLAlchemy typing incompatibilities and test double typing mismatches remain.
|
||||||
|
|
||||||
|
Key current blocker families:
|
||||||
|
|
||||||
|
1. SQLModel relationship/query attribute typing (`selectinload`, `order_by`, `.any()`)
|
||||||
|
2. SQLAlchemy join clause typing in `services/transcription.py`
|
||||||
|
3. Test fake client type mismatch for `OpenRouterTranscriptionProvider(client=...)`
|
||||||
|
4. `Settings(**defaults)` typed-dict strictness in `tests/test_config.py`
|
||||||
|
|
||||||
|
## Current Gate Status
|
||||||
|
|
||||||
|
- Lint: pass
|
||||||
|
- Non-external tests: pass
|
||||||
|
- External smoke tests: pass
|
||||||
|
- Type check: **blocked** (requires dedicated typing cleanup pass)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# V1 Traceability Matrix
|
||||||
|
|
||||||
|
This matrix provides implementation and validation evidence for V1 requirements (`REQ-0` through `REQ-13`).
|
||||||
|
|
||||||
|
Status values:
|
||||||
|
|
||||||
|
- `done`: implemented and evidence recorded
|
||||||
|
- `in progress`: partially implemented or evidence incomplete
|
||||||
|
- `not started`: no implementation/evidence yet
|
||||||
|
|
||||||
|
## Requirement Evidence Table
|
||||||
|
|
||||||
|
| Requirement | Status | Implementation Evidence | Validation Evidence |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| REQ-0 | done | End-to-end upload + worker pipeline in `src/transcription/services/store.py`, `src/transcription/worker.py`, `src/transcription/services/workflows.py` | `tests/integration/test_pipeline_flow.py` |
|
||||||
|
| REQ-1 | done | Upload UI/page flow in `src/transcription/ui/pages/upload_page.py`, `src/transcription/ui/components/upload.py` | `tests/ui/test_upload_page.py`, `tests/integration/test_pipeline_flow.py` |
|
||||||
|
| REQ-2 | done | Async worker execution and provider call orchestration in `src/transcription/worker.py`, `src/transcription/services/workflows.py` | `tests/integration/test_pipeline_flow.py`, `tests/services/test_workflows_reliability.py` |
|
||||||
|
| REQ-3 | done | Job lifecycle state model + transitions in `src/transcription/models.py`, `src/transcription/services/jobs.py`, `src/transcription/services/workflows.py` | `tests/services/test_job_service.py`, `tests/ui/test_jobs_page.py` |
|
||||||
|
| REQ-4 | done | Persistence of original output and failure detail in `src/transcription/services/transcription.py`, `src/transcription/services/workflows.py` | `tests/integration/test_pipeline_flow.py`, `tests/services/test_workflows_reliability.py` |
|
||||||
|
| REQ-5 | done | Status/result inspection via UI pages and API health route in `src/transcription/ui/pages/jobs_page.py`, `src/transcription/api/health.py` | `tests/ui/test_jobs_page.py`, `tests/ui/test_pages_registration.py`, `tests/api/test_health.py` |
|
||||||
|
| REQ-6 | done | Background processing trigger/worker notifier and non-blocking workflow in `src/transcription/ui/components/upload.py`, `src/transcription/worker.py` | `tests/test_app.py`, `tests/services/test_workflows_reliability.py` |
|
||||||
|
| REQ-7 | done | Lifespan-owned runtime resources in `src/transcription/app.py`, `src/transcription/db/runtime.py` | `tests/test_app.py`, `tests/test_db.py` |
|
||||||
|
| REQ-8 | done | Centralized settings/logging initialization in `src/transcription/config.py`, `src/transcription/app.py` | `tests/test_config.py`, `tests/test_app.py` |
|
||||||
|
| REQ-9 | done | Containerized runtime baseline in `docker-compose.yml`, `Dockerfile` | `docs/release_checklist_v1.md` (Ops checklist), manual demonstration step |
|
||||||
|
| REQ-10 | done | Explicit schema bootstrap policy + runtime controls in `src/transcription/config.py`, `src/transcription/app.py`, `src/transcription/db/operations.py` | `tests/test_db.py`, `tests/test_config.py` |
|
||||||
|
| REQ-11 | done | Service/workflow persistence boundaries in `src/transcription/services/*.py`, `src/transcription/services/workflows.py` | `tests/services/test_job_service.py`, `tests/services/test_transcription_service.py` |
|
||||||
|
| REQ-12 | done | Prompt artifacts in `prompts/` and loading/validation in `src/transcription/services/transcription.py` | `tests/test_prompts.py` |
|
||||||
|
| REQ-13 | done | Optional single revision create/update/view/delete in `src/transcription/services/transcription.py`, `src/transcription/ui/pages/jobs_page.py` | `tests/services/test_transcription_service.py`, `tests/ui/test_jobs_page.py` |
|
||||||
|
|
||||||
|
## Operational Evidence (Step 3 Artifacts)
|
||||||
|
|
||||||
|
- Runbook: `docs/runbook.md`
|
||||||
|
- Migration/backfill/rollback guidance: `docs/migration_v1.md`
|
||||||
|
- Release readiness checklist: `docs/release_checklist_v1.md`
|
||||||
|
|
||||||
|
## Verification Cadence
|
||||||
|
|
||||||
|
- Per change: maintain `tests/test_traceability.py` mappings for touched requirements.
|
||||||
|
- Per milestone: update this table status and evidence links.
|
||||||
|
- Pre-release: confirm all rows are `done` and non-external suite is green.
|
||||||
@@ -150,6 +150,9 @@ V1 is complete when all of the following are true:
|
|||||||
|
|
||||||
### Deliverables
|
### Deliverables
|
||||||
- V1 release checklist and acceptance evidence.
|
- V1 release checklist and acceptance evidence.
|
||||||
|
- `docs/runbook.md` for incident response and operator workflows.
|
||||||
|
- `docs/migration_v1.md` for V1 migration/backfill/rollback guidance.
|
||||||
|
- `docs/release_checklist_v1.md` for release sign-off.
|
||||||
|
|
||||||
### Exit Criteria
|
### Exit Criteria
|
||||||
- Stakeholder sign-off and launch readiness achieved.
|
- Stakeholder sign-off and launch readiness achieved.
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Version 2 Plan
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Version 2 extends the V1 baseline by introducing a production-oriented persistence architecture while preserving current user workflows.
|
||||||
|
|
||||||
|
Primary target changes:
|
||||||
|
|
||||||
|
- Migrate relational persistence from SQLite to PostgreSQL
|
||||||
|
- Introduce optional MongoDB for document-oriented adjunct data (non-canonical)
|
||||||
|
|
||||||
|
V1 behavior remains the functional baseline unless explicitly superseded by approved V2 requirements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## V2 Goals
|
||||||
|
|
||||||
|
1. **Relational migration complete**
|
||||||
|
- PostgreSQL becomes the default system of record for `Document`, `Source`, `Job`, and `Revision`.
|
||||||
|
2. **Operational maturity**
|
||||||
|
- Repeatable migrations, rollback paths, and environment-specific deployment procedures are documented and tested.
|
||||||
|
3. **Optional document store integration**
|
||||||
|
- MongoDB is introduced only for clearly scoped use cases that do not replace canonical relational ownership.
|
||||||
|
4. **No regression of V1 workflows**
|
||||||
|
- Upload, queue/worker processing, status inspection, original transcription, and optional single revision remain stable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-Goals (V2)
|
||||||
|
|
||||||
|
- Replacing SQLModel domain ownership with MongoDB
|
||||||
|
- Introducing breaking UI behavior for existing V1 flows
|
||||||
|
- Expanding revision cardinality beyond current `0..1` without explicit requirements update
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Scope
|
||||||
|
|
||||||
|
### A) PostgreSQL migration (required)
|
||||||
|
|
||||||
|
- Add PostgreSQL runtime profile for local/dev/prod
|
||||||
|
- Introduce migration toolchain and migration history
|
||||||
|
- Convert bootstrap strategy from compatibility patching to explicit migrations
|
||||||
|
- Validate model constraints and indexes against PostgreSQL
|
||||||
|
- Add operational checks (connectivity, pool, transaction behavior)
|
||||||
|
|
||||||
|
### B) MongoDB integration (optional, gated)
|
||||||
|
|
||||||
|
- Define approved use cases (for example: denormalized read models, audit/event projections, or search-oriented materializations)
|
||||||
|
- Keep canonical write path in relational store
|
||||||
|
- Add feature flag/config gate to enable or disable Mongo features
|
||||||
|
- Document consistency model and failure behavior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
## M1 — Requirements and architecture baseline
|
||||||
|
|
||||||
|
- Create V2 requirements delta from V1 baseline
|
||||||
|
- Define relational/document ownership boundaries
|
||||||
|
- Approve migration strategy and cutover approach
|
||||||
|
|
||||||
|
**Exit criteria:** signed architecture decision and updated traceability map.
|
||||||
|
|
||||||
|
## M2 — PostgreSQL foundation
|
||||||
|
|
||||||
|
- Add PostgreSQL environment wiring and secrets strategy
|
||||||
|
- Add migration framework and initial schema migration
|
||||||
|
- Add CI path using PostgreSQL service container
|
||||||
|
|
||||||
|
**Exit criteria:** test suite green on PostgreSQL in CI.
|
||||||
|
|
||||||
|
## M3 — Data migration and cutover rehearsal
|
||||||
|
|
||||||
|
- Build SQLite -> PostgreSQL migration utility/playbook
|
||||||
|
- Rehearse migration on representative datasets
|
||||||
|
- Validate rollback/recovery procedures
|
||||||
|
|
||||||
|
**Exit criteria:** successful dry-run migration with measured rollback test.
|
||||||
|
|
||||||
|
## M4 — MongoDB optional integration
|
||||||
|
|
||||||
|
- Implement scoped Mongo use case(s)
|
||||||
|
- Add fallback behavior when Mongo unavailable
|
||||||
|
- Add tests and operational runbook updates
|
||||||
|
|
||||||
|
**Exit criteria:** feature-gated Mongo behavior validated with no V1 flow regressions.
|
||||||
|
|
||||||
|
## M5 — Release readiness
|
||||||
|
|
||||||
|
- Final regression suite (functional + reliability)
|
||||||
|
- Performance and failure-mode checks
|
||||||
|
- Production release checklist and sign-off
|
||||||
|
|
||||||
|
**Exit criteria:** V2 release approval.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
- **Schema drift risk** -> enforce migration-first policy and CI migration checks.
|
||||||
|
- **Dual-store consistency risk** -> keep relational source of truth and explicit projection contracts.
|
||||||
|
- **Operational complexity** -> staged rollout, runbooks, and feature flags.
|
||||||
|
- **Regression risk in worker lifecycle** -> keep dedicated reliability tests around terminal-state guarantees.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Traceability and Evidence
|
||||||
|
|
||||||
|
Maintain a V2 table with:
|
||||||
|
|
||||||
|
- requirement/change ID
|
||||||
|
- status (`not started` / `in progress` / `done`)
|
||||||
|
- implementation PR
|
||||||
|
- validation evidence (test names, migration rehearsal logs, runbook references)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested first implementation tasks
|
||||||
|
|
||||||
|
1. Create `docs/ver2/adr/` and draft ADR for persistence ownership boundaries.
|
||||||
|
2. Add PostgreSQL compose profile and env contract.
|
||||||
|
3. Introduce migration tooling and generate initial migration from current schema.
|
||||||
|
4. Add CI job for PostgreSQL-backed `pytest -m "not external"`.
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import UTC
|
from datetime import UTC
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
@@ -26,7 +26,6 @@ from .services.jobs import JobService
|
|||||||
from .ui import register_pages
|
from .ui import register_pages
|
||||||
from .worker import worker_consumer_lifespan
|
from .worker import worker_consumer_lifespan
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,5 +67,13 @@ def _ensure_sqlite_compat_columns(connection: Connection) -> None:
|
|||||||
has_unique_source = True
|
has_unique_source = True
|
||||||
break
|
break
|
||||||
if not has_unique_source:
|
if not has_unique_source:
|
||||||
connection.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ux_revision_source_id ON revision(source_id)"))
|
connection.execute(
|
||||||
logger.warning("Applied SQLite compatibility schema patch table=revision unique_index=ux_revision_source_id")
|
text(
|
||||||
|
"CREATE UNIQUE INDEX IF NOT EXISTS "
|
||||||
|
"ux_revision_source_id ON revision(source_id)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.warning(
|
||||||
|
"Applied SQLite compatibility schema patch "
|
||||||
|
"table=revision unique_index=ux_revision_source_id"
|
||||||
|
)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ async def process_queued_job(
|
|||||||
source.id,
|
source.id,
|
||||||
result.provider,
|
result.provider,
|
||||||
)
|
)
|
||||||
except TimeoutError as exc:
|
except TimeoutError:
|
||||||
error = AppError(
|
error = AppError(
|
||||||
f"Provider call timed out after {runtime_settings.worker_provider_timeout_seconds:.1f}s",
|
f"Provider call timed out after {runtime_settings.worker_provider_timeout_seconds:.1f}s",
|
||||||
category=ErrorCategory.EXTERNAL_PROVIDER,
|
category=ErrorCategory.EXTERNAL_PROVIDER,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from ..components.transcript import render_original_transcription_card
|
|||||||
from ..components.transcript import render_revision_row
|
from ..components.transcript import render_revision_row
|
||||||
|
|
||||||
|
|
||||||
def register_page() -> None:
|
def register_page() -> None: # noqa: PLR0915
|
||||||
"""Register jobs list and detail routes."""
|
"""Register jobs list and detail routes."""
|
||||||
|
|
||||||
@ui.page("/jobs")
|
@ui.page("/jobs")
|
||||||
@@ -51,7 +51,7 @@ def register_page() -> None:
|
|||||||
await render_table()
|
await render_table()
|
||||||
|
|
||||||
@ui.page("/jobs/{job_id}")
|
@ui.page("/jobs/{job_id}")
|
||||||
async def job_detail_page(job_id: str, request: Request) -> None:
|
async def job_detail_page(job_id: str, request: Request) -> None: # noqa: PLR0915
|
||||||
session_factory = resolve_session_factory(request.app.state)
|
session_factory = resolve_session_factory(request.app.state)
|
||||||
jobs_service = JobService(session_factory=session_factory)
|
jobs_service = JobService(session_factory=session_factory)
|
||||||
transcription_service = TranscriptionService(session_factory=session_factory)
|
transcription_service = TranscriptionService(session_factory=session_factory)
|
||||||
@@ -105,14 +105,52 @@ def register_page() -> None:
|
|||||||
async def render_revision_panel() -> None:
|
async def render_revision_panel() -> None:
|
||||||
refreshed_job = await jobs_service.read_job(job_id=parsed_job_id)
|
refreshed_job = await jobs_service.read_job(job_id=parsed_job_id)
|
||||||
refreshed_source = _resolve_primary_source(refreshed_job)
|
refreshed_source = _resolve_primary_source(refreshed_job)
|
||||||
if refreshed_source is None or refreshed_source.revision is None:
|
if refreshed_source is None:
|
||||||
|
ui.label("No source is available for revision editing.").classes("text-body2 text-grey-3")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_revision = refreshed_source.revision
|
||||||
|
default_revision_text = (
|
||||||
|
current_revision.text if current_revision is not None else (refreshed_job.text or "")
|
||||||
|
)
|
||||||
|
|
||||||
|
ui.label("Revision Editor").classes("text-subtitle1 text-weight-medium")
|
||||||
|
editor = ui.textarea(label="Revision text", value=default_revision_text).props("autogrow outlined")
|
||||||
|
editor.classes("w-full")
|
||||||
|
|
||||||
|
async def save_revision() -> None:
|
||||||
|
candidate = (editor.value or "").strip()
|
||||||
|
if not candidate:
|
||||||
|
ui.notify("Revision text is required.", type="warning")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await transcription_service.upsert_revision_for_source(
|
||||||
|
source_id=refreshed_source.id,
|
||||||
|
text=candidate,
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
show_error(exc, title="Save failed", operation="jobs.save_revision")
|
||||||
|
return
|
||||||
|
|
||||||
|
ui.notify("Revision saved", type="positive")
|
||||||
|
await render_revision_panel.refresh()
|
||||||
|
|
||||||
|
with ui.row().classes("w-full justify-end"):
|
||||||
|
ui.button(
|
||||||
|
"Create revision" if current_revision is None else "Update revision",
|
||||||
|
on_click=save_revision,
|
||||||
|
icon="save",
|
||||||
|
).props('unelevated color="primary"')
|
||||||
|
|
||||||
|
if current_revision is None:
|
||||||
ui.label("No revision exists for this source.").classes("text-body2 text-grey-3")
|
ui.label("No revision exists for this source.").classes("text-body2 text-grey-3")
|
||||||
return
|
return
|
||||||
|
|
||||||
render_revision_row(
|
render_revision_row(
|
||||||
revision=refreshed_source.revision,
|
revision=current_revision,
|
||||||
initially_expanded=True,
|
initially_expanded=True,
|
||||||
on_delete=lambda _revision, rid=refreshed_source.revision.id: delete_revision_by_id(rid),
|
on_delete=lambda _revision, rid=current_revision.id: delete_revision_by_id(rid),
|
||||||
)
|
)
|
||||||
|
|
||||||
await render_revision_panel()
|
await render_revision_panel()
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"""Tests for API error response envelope handlers."""
|
"""Tests for API error response envelope handlers."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
import pytest
|
|
||||||
|
|
||||||
from transcription.api.errors import register_error_handlers
|
from transcription.api.errors import register_error_handlers
|
||||||
from transcription.errors import AppError, ErrorCategory
|
from transcription.errors import AppError
|
||||||
|
from transcription.errors import ErrorCategory
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ BOOK 1 had 54 pages; 14 chapters. BOOK 2 has 70 pages; 18 chapters. BOOK 1 con-
|
|||||||
sisted largely of first generation family history. BOOK 2 throws more light on
|
sisted largely of first generation family history. BOOK 2 throws more light on
|
||||||
the second generation. Sidney promises a BOOK 3 and that may begin to do justice
|
the second generation. Sidney promises a BOOK 3 and that may begin to do justice
|
||||||
to the third generation. We suggest that Sidney get the help of Louis Shinn
|
to the third generation. We suggest that Sidney get the help of Louis Shinn
|
||||||
who has a chapter in this book (Chapter 16 - The Last 25 Years on the Doumeeq
|
who has a chapter in this book (Chapter 16 - The Last 25 Years on the Doumecq
|
||||||
Plains. Louis has the gift of seeing, recalling and telling. One sentence in
|
Plains. Louis has the gift of seeing, recalling and telling. One sentence in
|
||||||
his chapter gives a great tribute to the Doumeeqers - so far as he knows no one
|
his chapter gives a great tribute to the Doumeccqers--so [sic] far as he knows no one
|
||||||
on the Doumeeq Plains went on relief during the depression. That in a nutshell
|
on the Doumecq Plains went on relief during the depression. That in a nutshell
|
||||||
shows the sturdy character of the residents of the Doumeeq Plains.
|
shows the sturdy character of the residents of the Doumecq Plains.
|
||||||
|
|
||||||
We promised in BOOK 1 that in BOOK 2 we would give the story of the trip of
|
We promised in BOOK 1 that in BOOK 2 we would give the story of the trip of
|
||||||
John E. Cochran and wife to Tennessee, Cuba and the Panama Canal. You will see
|
John E. Cochran and wife to Tennessee, Cuba and the Panama Canal. You will see
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ the family newsletter two years ago.
|
|||||||
|
|
||||||
Nome Alaska August 26, 1923
|
Nome Alaska August 26, 1923
|
||||||
My Dear Ethel et al.
|
My Dear Ethel et al.
|
||||||
|
|
||||||
I don't know when I did write or when you did
|
I don't know when I did write or when you did
|
||||||
but I am going to write now however and never
|
but I am going to write now however and never
|
||||||
the less. But I wish I could talk (I can yet but I
|
the less. But I wish I could talk (I can yet but I
|
||||||
@@ -27,7 +26,6 @@ and Polly sit up and listen and that little black
|
|||||||
rascal of yours would fairly sparkle with
|
rascal of yours would fairly sparkle with
|
||||||
listening. Can't I see him listening now to all the
|
listening. Can't I see him listening now to all the
|
||||||
yarns we told last summer?
|
yarns we told last summer?
|
||||||
|
|
||||||
You see, we-Miss Saville and I, took a trip north
|
You see, we-Miss Saville and I, took a trip north
|
||||||
on the Buford and it was very interesting. We
|
on the Buford and it was very interesting. We
|
||||||
went north thru the Bering Strait into the Arctic and as far as the Ice Pack. There the captain
|
went north thru the Bering Strait into the Arctic and as far as the Ice Pack. There the captain
|
||||||
@@ -43,14 +41,14 @@ all around it similar to a currycomb in coarseness; no ears but huge tusks of iv
|
|||||||
the most repulsive looking animals imaginable and tho I have always read about them I never
|
the most repulsive looking animals imaginable and tho I have always read about them I never
|
||||||
expect such disagreeable looking creatures. They had a rough brown hairy skin and some of
|
expect such disagreeable looking creatures. They had a rough brown hairy skin and some of
|
||||||
them looked warty. They must have weighed two ton at least. Ere we got them back to Nome
|
them looked warty. They must have weighed two ton at least. Ere we got them back to Nome
|
||||||
to the natives they were getting extremely odiferous–in fact, you could scarcely stay on the
|
to the natives they were getting extremely odiferous—in fact, you could scarcely stay on the
|
||||||
ship with any degree of comfort unless you had per chance lost your sense of smell.
|
ship with any degree of comfort unless you had per chance lost your sense of smell.
|
||||||
|
|
||||||
Then we went north to a few minutes beyond the 70th degree of latitude and thot [sic] for awhile
|
Then we went north to a few minutes beyond the 70th degree of latitude and thot for awhile
|
||||||
we would go to Wrangell Island where some men from Stefflonsons [sic] ship were supposed to be
|
we would go to Wrangell Island where some men from Steffonsons ship were supposed to be
|
||||||
stranded but we didn't get there and instead stopped at a small native village at Cape Serdz [sic] in
|
stranded but we didn't get there and instead stopped at a small native village at Cape Serdz in
|
||||||
Siberia. These Eskimo were very primitive. One white squaw man lived there and had for 23
|
Siberia. These Eskimo were very primitive. One white squaw man lived there and had for 23
|
||||||
years. He was a Swede–who else could. Their houses were circular and built up with dirt 2 or
|
years. He was a Swede--who else could. Their houses were circular and built up with dirt 2 or
|
||||||
3 feet and then skins were stretched over it and weighted down with rocks. Inside, the room
|
3 feet and then skins were stretched over it and weighted down with rocks. Inside, the room
|
||||||
was partitioned off at the sides with skins for sleeping quarters. In the main part they had the
|
was partitioned off at the sides with skins for sleeping quarters. In the main part they had the
|
||||||
fire on the ground and the fish drying on lines and the skins hanging around and the dogs and
|
fire on the ground and the fish drying on lines and the skins hanging around and the dogs and
|
||||||
@@ -65,22 +63,22 @@ The other place we stopped was at Whalen, a trading post in Siberia. There these
|
|||||||
went wild. They rushed helter-skelter, hither and thither, here and there, trying to find
|
went wild. They rushed helter-skelter, hither and thither, here and there, trying to find
|
||||||
something to buy. Prices raised right before your eyes. One would but something for $1.00
|
something to buy. Prices raised right before your eyes. One would but something for $1.00
|
||||||
and the next might have to pay $2.00, $4.00 or $10.00. That made no difference. They had to
|
and the next might have to pay $2.00, $4.00 or $10.00. That made no difference. They had to
|
||||||
have it. One man I was sort of taking care of, tho [sic] he had his son along for the purpose,
|
have it. One man I was sort of taking care of, tho he had his son along for the purpose,
|
||||||
bought 2 ivory tusks, 1 pup, 2 moccasins, 3 or 4 billi[illegible]s, 6 or 8 ivory and silver rings, one
|
bought 2 ivory tusks, 1 pup, 2 moccasins, 3 or 4 billikens, 6 or 8 ivory and silver rings, one
|
||||||
fishing line, hooks, floats, etc. and two bird slings. The slings have rocks at the end and the
|
fishing line, hooks, floats, etc. and two bird slings. The slings have rocks at the end and the
|
||||||
little natives throw them at the flocks of geese and ducks which fly close over the village and
|
little natives throw them at the flocks of geese and ducks which fly close over the village and
|
||||||
the slings entangle their wings and legs, sometimes more than one, and they can't fly. They
|
the slings entangle their wings and legs, sometimes more than one, and they can't fly. They
|
||||||
come down and the natives capture them. There was more junk brot [sic] aboard than baggage, I
|
come down and the natives capture them. There was more junk brot aboard than baggage, I
|
||||||
do believe. And they say that at the first stop it was worse than here. The red flag was flying
|
do believe. And they say that at the first stop it was worse than here. The red flag was flying
|
||||||
over Whalen and the Russian soldiers were there–a few, one or two or three, I forget the
|
over Whalen and the Russian soldiers were there—a few, one or two or three, I forget the
|
||||||
number.
|
number.
|
||||||
|
|
||||||
We got home yesterday morning at 5 a.m. but missed the first lighter in so had to stay out
|
We got home yesterday morning at 5 a.m. but missed the first lighter in so had to stay out
|
||||||
until 2:30. The girls had prepared a big meal for us and invited up the Hartfords and then let
|
until 2:30. The girls had prepared a big meal for us and invited up the Hartfords and then let
|
||||||
us talk. Miss Saville talked quite a bit. Any how if you folks don't like this I don't care, it is
|
us talk. Miss Saville talked quite a bit. Any how if you folks don't like this I don't care, it is
|
||||||
all I had to write about and I know Buster'd [sic] listen anyway and I'd soak ole Peter's head if he
|
all I had to write about and I know Buster'ud listen anyway and I'd soak ole Peter's head if he
|
||||||
didn't and Polly would in my lap and I don't know much about the youngest one of yours so
|
didn't and Polly would in my lap and I don't know much about the youngest one of yours so
|
||||||
likely he would be squawling. But we did surely enjoy our trip and were gone just long enuf [sic].
|
likely he would be squawling. But we did surely enjoy our trip and were gone just long enuf.
|
||||||
|
|
||||||
I expect there were 150 passengers on board and almost or more of the crew and helpers. We
|
I expect there were 150 passengers on board and almost or more of the crew and helpers. We
|
||||||
had a stateroom down next to the kitchen and 'twas pretty fierce for odor at times.
|
had a stateroom down next to the kitchen and 'twas pretty fierce for odor at times.
|
||||||
@@ -109,7 +107,7 @@ Ome
|
|||||||
|
|
||||||
Reprinted from Cochran Chronicles, Volume 9, Number 1, November 1986
|
Reprinted from Cochran Chronicles, Volume 9, Number 1, November 1986
|
||||||
|
|
||||||
© [inserted: JECFA] 1986
|
© JECFA 1986
|
||||||
|
|
||||||
Up
|
Up
|
||||||
|
|
||||||
|
|||||||
@@ -8,24 +8,23 @@ ISBILL & MOSER
|
|||||||
DEALERS IN
|
DEALERS IN
|
||||||
GENERAL MERCHANDISE
|
GENERAL MERCHANDISE
|
||||||
|
|
||||||
Vonore, Tenn., Jany 27- 1913
|
Vonore, Tenn. Jany 27- 1913
|
||||||
Dear Much Aunt Louie
|
Dear Much Aunt Adeline
|
||||||
How are you a
|
Was at home a
|
||||||
few nights ago I sewed a
|
few nights ago & saw a
|
||||||
letter from your folks, so
|
letter from your folks. So
|
||||||
I decided to write you
|
I decided to write you
|
||||||
a few lines myself ok
|
a few lines myself ok
|
||||||
I am contemplateing a
|
I am contemplateing [sic] a
|
||||||
trip out west next summer
|
trip out west next summer
|
||||||
& I want Some Olders to go
|
& [inserted: I] want some of them to go
|
||||||
where I and them.
|
when I am [inserted: a] them.
|
||||||
|
Am getting
|
||||||
I am getting
|
|
||||||
up in years & unmarried
|
up in years & unmarried
|
||||||
so you see the object of
|
so you see the object of
|
||||||
my trip, is to get a bunch
|
my trip, is to get a wife
|
||||||
of Young & old maids
|
& if there is any old maids
|
||||||
& widows out there. I
|
or widows out there, I
|
||||||
want you to kiss them
|
want you to kiss them
|
||||||
at my fans [sic] mug as they
|
at my [hand?] me at there
|
||||||
as soon as I get there
|
as soon as I get there.
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ class TestPipelineSuccessFlow:
|
|||||||
|
|
||||||
async def _fake_transcribe(*, prompt_text: str, image_bytes: bytes, mime_type: str) -> TranscriptionResult:
|
async def _fake_transcribe(*, prompt_text: str, image_bytes: bytes, mime_type: str) -> TranscriptionResult:
|
||||||
_ = (prompt_text, image_bytes, mime_type)
|
_ = (prompt_text, image_bytes, mime_type)
|
||||||
return TranscriptionResult(text="Pipeline transcript", provider="openrouter", model="test-model", prompt_name="transcribe_document.md")
|
return TranscriptionResult(
|
||||||
|
text="Pipeline transcript",
|
||||||
|
provider="openrouter",
|
||||||
|
model="test-model",
|
||||||
|
prompt_name="transcribe_document.md",
|
||||||
|
)
|
||||||
|
|
||||||
async def _fake_transcribe_document_image(
|
async def _fake_transcribe_document_image(
|
||||||
image_path,
|
image_path,
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ from types import SimpleNamespace
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from transcription.config import Settings
|
from transcription.config import Settings
|
||||||
from transcription.providers.base import ProviderError, ProviderResponseError
|
from transcription.providers.base import ProviderError
|
||||||
from transcription.providers.openrouter import DEFAULT_OPENROUTER_MODEL, OpenRouterTranscriptionProvider
|
from transcription.providers.base import ProviderResponseError
|
||||||
|
from transcription.providers.openrouter import DEFAULT_OPENROUTER_MODEL
|
||||||
|
from transcription.providers.openrouter import OpenRouterTranscriptionProvider
|
||||||
|
|
||||||
|
|
||||||
class _FakeChat:
|
class _FakeChat:
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ class TestJobService:
|
|||||||
assert fetched.document.id == document.id
|
assert fetched.document.id == document.id
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_job_state_updates_status_and_retry(self, job_service: JobService, document_service: DocumentService):
|
async def test_update_job_state_updates_status_and_retry(
|
||||||
|
self,
|
||||||
|
job_service: JobService,
|
||||||
|
document_service: DocumentService,
|
||||||
|
):
|
||||||
document = Document(id=uuid4(), name="test-bundle")
|
document = Document(id=uuid4(), name="test-bundle")
|
||||||
await document_service.create_document(document=document)
|
await document_service.create_document(document=document)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import pytest
|
|||||||
|
|
||||||
from transcription.services.transcription import transcribe_document_image
|
from transcription.services.transcription import transcribe_document_image
|
||||||
|
|
||||||
|
|
||||||
HAS_OPENROUTER_KEY = bool(os.getenv("OPENROUTER_API_KEY"))
|
HAS_OPENROUTER_KEY = bool(os.getenv("OPENROUTER_API_KEY"))
|
||||||
|
|
||||||
REAL_IMAGES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "real"
|
REAL_IMAGES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "real"
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"""Tests for revision behavior in TranscriptionService."""
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from transcription.models import Document
|
||||||
|
from transcription.models import Job
|
||||||
|
from transcription.models import JobStatus
|
||||||
|
from transcription.models import Source
|
||||||
|
from transcription.services.documents import DocumentService
|
||||||
|
from transcription.services.jobs import JobService
|
||||||
|
from transcription.services.transcription import TranscriptionService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
class TestTranscriptionServiceRevisionUpsert:
|
||||||
|
"""Verify optional single-revision create/update semantics."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_upsert_revision_creates_new_revision(self, default_session_factory):
|
||||||
|
documents = DocumentService(session_factory=default_session_factory)
|
||||||
|
jobs = JobService(session_factory=default_session_factory)
|
||||||
|
transcriptions = TranscriptionService(session_factory=default_session_factory)
|
||||||
|
|
||||||
|
document = Document(id=uuid4(), name="revision-create")
|
||||||
|
await documents.create_document(document=document)
|
||||||
|
|
||||||
|
job = Job(document_id=document.id, status=JobStatus.TRANSCRIBED, text="Original text")
|
||||||
|
await jobs.create_job(job=job)
|
||||||
|
|
||||||
|
source = Source(
|
||||||
|
document_id=document.id,
|
||||||
|
job_id=job.id,
|
||||||
|
upload_name="source.jpg",
|
||||||
|
filename="source.jpg",
|
||||||
|
file_path="uploads/source.jpg",
|
||||||
|
)
|
||||||
|
async with transcriptions._session_scope() as session:
|
||||||
|
session.add(source)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(source)
|
||||||
|
|
||||||
|
revision = await transcriptions.upsert_revision_for_source(source_id=source.id, text="User revision")
|
||||||
|
fetched = await transcriptions.read_revision_by_source(source.id)
|
||||||
|
|
||||||
|
assert revision.source_id == source.id
|
||||||
|
assert revision.text == "User revision"
|
||||||
|
assert fetched is not None
|
||||||
|
assert fetched.id == revision.id
|
||||||
|
assert fetched.text == "User revision"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_upsert_revision_updates_existing_single_revision(self, default_session_factory):
|
||||||
|
documents = DocumentService(session_factory=default_session_factory)
|
||||||
|
jobs = JobService(session_factory=default_session_factory)
|
||||||
|
transcriptions = TranscriptionService(session_factory=default_session_factory)
|
||||||
|
|
||||||
|
document = Document(id=uuid4(), name="revision-update")
|
||||||
|
await documents.create_document(document=document)
|
||||||
|
|
||||||
|
job = Job(document_id=document.id, status=JobStatus.TRANSCRIBED, text="Original text")
|
||||||
|
await jobs.create_job(job=job)
|
||||||
|
|
||||||
|
source = Source(
|
||||||
|
document_id=document.id,
|
||||||
|
job_id=job.id,
|
||||||
|
upload_name="source.jpg",
|
||||||
|
filename="source.jpg",
|
||||||
|
file_path="uploads/source.jpg",
|
||||||
|
)
|
||||||
|
async with transcriptions._session_scope() as session:
|
||||||
|
session.add(source)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(source)
|
||||||
|
|
||||||
|
first = await transcriptions.upsert_revision_for_source(source_id=source.id, text="Revision v1")
|
||||||
|
second = await transcriptions.upsert_revision_for_source(source_id=source.id, text="Revision v2")
|
||||||
|
revisions = await transcriptions.list_revisions_by_job(job.id)
|
||||||
|
|
||||||
|
assert first.id == second.id
|
||||||
|
assert second.text == "Revision v2"
|
||||||
|
assert len(revisions) == 1
|
||||||
|
assert revisions[0].id == first.id
|
||||||
|
assert revisions[0].text == "Revision v2"
|
||||||
@@ -22,9 +22,21 @@ class TestWorkflowReliability:
|
|||||||
async def test_process_queued_job_timeout_marks_job_failed(self, default_session_factory, monkeypatch):
|
async def test_process_queued_job_timeout_marks_job_failed(self, default_session_factory, monkeypatch):
|
||||||
"""Provider timeout transitions a queued job to failed with error detail."""
|
"""Provider timeout transitions a queued job to failed with error detail."""
|
||||||
services = ServiceBundle()
|
services = ServiceBundle()
|
||||||
object.__setattr__(services, "documents", services.documents.__class__(session_factory=default_session_factory))
|
object.__setattr__(
|
||||||
object.__setattr__(services, "jobs", services.jobs.__class__(session_factory=default_session_factory))
|
services,
|
||||||
object.__setattr__(services, "transcriptions", services.transcriptions.__class__(session_factory=default_session_factory))
|
"documents",
|
||||||
|
services.documents.__class__(session_factory=default_session_factory),
|
||||||
|
)
|
||||||
|
object.__setattr__(
|
||||||
|
services,
|
||||||
|
"jobs",
|
||||||
|
services.jobs.__class__(session_factory=default_session_factory),
|
||||||
|
)
|
||||||
|
object.__setattr__(
|
||||||
|
services,
|
||||||
|
"transcriptions",
|
||||||
|
services.transcriptions.__class__(session_factory=default_session_factory),
|
||||||
|
)
|
||||||
|
|
||||||
async with services.jobs._session_scope() as session:
|
async with services.jobs._session_scope() as session:
|
||||||
document = Document(id=uuid4(), name="timeout-doc")
|
document = Document(id=uuid4(), name="timeout-doc")
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from transcription.config import Provider, Settings
|
from transcription.config import Provider
|
||||||
|
from transcription.config import Settings
|
||||||
|
|
||||||
|
|
||||||
def _make_settings(**overrides) -> Settings:
|
def _make_settings(**overrides) -> Settings:
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from transcription.errors import AppError, ErrorCategory, classify_unexpected_error, new_error_id
|
from transcription.errors import AppError
|
||||||
|
from transcription.errors import ErrorCategory
|
||||||
|
from transcription.errors import classify_unexpected_error
|
||||||
|
from transcription.errors import new_error_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ from uuid import UUID
|
|||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from transcription.models import Document, Job, JobStatus, Revision, Source
|
from transcription.models import Document
|
||||||
|
from transcription.models import Job
|
||||||
|
from transcription.models import JobStatus
|
||||||
|
from transcription.models import Revision
|
||||||
|
from transcription.models import Source
|
||||||
|
|
||||||
|
|
||||||
def _make_document(**overrides) -> Document:
|
def _make_document(**overrides) -> Document:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
PROMPT_PATH = Path("prompts/transcribe_document.md")
|
PROMPT_PATH = Path("prompts/transcribe_document.md")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -71,3 +71,37 @@ class TestPageRendering:
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "Job not found" in response.text
|
assert "Job not found" in response.text
|
||||||
|
|
||||||
|
def test_job_detail_page_shows_revision_editor_when_none_exists(self, app_client, seed_job):
|
||||||
|
"""GET /ui/jobs/{job_id} renders revision editor and create action for sources with no revision."""
|
||||||
|
_, client = app_client
|
||||||
|
job_id = seed_job(
|
||||||
|
filename="no-revision.pdf",
|
||||||
|
status=JobStatus.TRANSCRIBED,
|
||||||
|
transcription_text="original text",
|
||||||
|
revision_text=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(f"/ui/jobs/{job_id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Revision Editor" in response.text
|
||||||
|
assert "Create revision" in response.text
|
||||||
|
assert "No revision exists for this source." in response.text
|
||||||
|
|
||||||
|
def test_job_detail_page_shows_update_action_for_existing_revision(self, app_client, seed_job):
|
||||||
|
"""GET /ui/jobs/{job_id} renders revision editor with update action when revision exists."""
|
||||||
|
_, client = app_client
|
||||||
|
job_id = seed_job(
|
||||||
|
filename="with-revision.pdf",
|
||||||
|
status=JobStatus.TRANSCRIBED,
|
||||||
|
transcription_text="original text",
|
||||||
|
revision_text="hello",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(f"/ui/jobs/{job_id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Revision Editor" in response.text
|
||||||
|
assert "Update revision" in response.text
|
||||||
|
assert "hello" in response.text
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user