Files
transcription/Untitled-1.ipynb
T

83 KiB

In [1]:
%load_ext autoreload
%autoreload 2
In [ ]:
from pathlib import Path
from uuid import UUID

import rich

from transcription.config import configure_logging
from transcription.config import get_settings
from transcription.db.operations import create_all
from transcription.db.runtime import get_engine
from transcription.db.runtime import get_session
from transcription.models import Document
from transcription.models import Job
from transcription.models import JobStatus
from transcription.services import ServiceBundle
from transcription.services.documents import DocumentService
from transcription.services.documents import create_upload_job
from transcription.services.jobs import JobService
from transcription.services.store import store_file
from transcription.worker import process_queued_job

configure_logging()
settings = get_settings(worker_max_retries=2)
engine = get_engine(settings=settings)
await create_all(engine=engine)
services = ServiceBundle()
2026-06-27 22:09:01 DEBUG    | Logging configured
2026-06-27 22:09:01 DEBUG    | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db
2026-06-27 22:09:01 DEBUG    | Database schema bootstrap complete for database_url=sqlite+aiosqlite:///./transcription.db
In [3]:
jobs = await services.jobs.list_jobs()
rich.print(jobs)
[
    Job(
        id=UUID('c79d20d1-ab5c-4b3b-a898-13eac7d2c71a'),
        retry_count=0,
        updated_at=datetime.datetime(2026, 6, 28, 2, 55, 13, 69063),
        document_id=UUID('105e84b2-d773-4abb-bd49-8dc26b966046'),
        status=<JobStatus.TRANSCRIBED: 'transcribed'>,
        created_at=datetime.datetime(2026, 6, 28, 2, 55, 13, 69054)
    ),
    Job(
        id=UUID('271b688e-63c7-4652-9ccf-5c1dd3f61058'),
        retry_count=0,
        updated_at=datetime.datetime(2026, 6, 28, 2, 55, 17, 189346),
        document_id=UUID('06f49121-9b98-4b0e-b879-7a3b959fe984'),
        status=<JobStatus.TRANSCRIBED: 'transcribed'>,
        created_at=datetime.datetime(2026, 6, 28, 2, 55, 17, 189338)
    ),
    Job(
        id=UUID('4386391c-3ce9-412a-9bd4-46b3860d0ef7'),
        retry_count=0,
        updated_at=datetime.datetime(2026, 6, 28, 2, 55, 19, 161570),
        document_id=UUID('ac04c024-25ca-4707-8cd8-641606976344'),
        status=<JobStatus.TRANSCRIBED: 'transcribed'>,
        created_at=datetime.datetime(2026, 6, 28, 2, 55, 19, 161560)
    ),
    Job(
        id=UUID('6637b36d-82e1-41e1-8776-037183851d30'),
        retry_count=0,
        updated_at=datetime.datetime(2026, 6, 28, 2, 55, 26, 921982),
        document_id=UUID('9c6d8632-fe6b-4f70-bb9b-5e8d762b4b3e'),
        status=<JobStatus.TRANSCRIBED: 'transcribed'>,
        created_at=datetime.datetime(2026, 6, 28, 2, 55, 26, 921974)
    ),
    Job(
        id=UUID('758f457a-343c-4b4a-98af-278875303803'),
        retry_count=0,
        updated_at=datetime.datetime(2026, 6, 28, 3, 4, 40, 999438),
        document_id=UUID('0fb70d21-6409-40cc-83df-abce2917f367'),
        status=<JobStatus.TRANSCRIBED: 'transcribed'>,
        created_at=datetime.datetime(2026, 6, 28, 3, 4, 40, 999427)
    ),
    Job(
        id=UUID('01060a33-464d-48c9-b4bb-f1f6acd91820'),
        retry_count=0,
        updated_at=datetime.datetime(2026, 6, 28, 3, 7, 6, 284063),
        document_id=UUID('c8d0eacb-87dd-4d88-a576-ce810436dc45'),
        status=<JobStatus.TRANSCRIBED: 'transcribed'>,
        created_at=datetime.datetime(2026, 6, 28, 3, 7, 6, 284053)
    ),
    Job(
        id=UUID('50800d67-fece-452e-b3ff-b7045fb1838f'),
        retry_count=0,
        updated_at=datetime.datetime(2026, 6, 28, 3, 8, 2, 737235),
        document_id=UUID('273a58fd-4c4d-4e3a-93dd-4f391650e646'),
        status=<JobStatus.TRANSCRIBED: 'transcribed'>,
        created_at=datetime.datetime(2026, 6, 28, 3, 8, 2, 737226)
    )
]
In [4]:
doc_path = Path("uploads/7c6ee648-4171-4e70-b901-ba94ee227d6a_Time Rolls On - page 013.jpg")
doc_path.name.rpartition("_")[2]
Out [4]:
'Time Rolls On - page 013.jpg'
In [5]:
async with get_session(settings=settings) as session:
    result = await create_upload_job(
        session=session,
        filename=doc_path.name.rpartition("_")[2],
        file_bytes=doc_path.read_bytes(),
        settings=settings,
    )

rich.print(result)
2026-06-27 22:09:01 DEBUG    | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db
2026-06-27 22:09:01 INFO     | Stored uploaded file: uploads/9d3ce411-e027-4e97-9c11-786783926ce4_Time Rolls On - page 013.jpg
2026-06-27 22:09:01 INFO     | Created upload job document_id=7102ad0c-dcc3-4164-a86f-4e601fe31e70 job_id=5bb45fc1-6b3b-4e92-9942-f8f059baaaaf
UploadJobResult(
    document_id=UUID('7102ad0c-dcc3-4164-a86f-4e601fe31e70'),
    job_id=UUID('5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'),
    stored_path=PosixPath('uploads/9d3ce411-e027-4e97-9c11-786783926ce4_Time Rolls On - page 013.jpg'),
    original_filename='Time Rolls On - page 013.jpg'
)
In [6]:
import random

job = random.choice(await services.jobs.query_jobs(status=JobStatus.QUEUED))
rich.print(job)
Job(
    id=UUID('5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'),
    retry_count=0,
    updated_at=datetime.datetime(2026, 6, 28, 3, 9, 1, 351464),
    document_id=UUID('7102ad0c-dcc3-4164-a86f-4e601fe31e70'),
    status=<JobStatus.QUEUED: 'queued'>,
    created_at=datetime.datetime(2026, 6, 28, 3, 9, 1, 351451)
)
In [7]:
async with get_session(settings=settings) as session:
    jobs = await services.jobs.query_jobs(
        status=JobStatus.QUEUED,
        session=session,
    )

    job = random.choice(jobs)
    job = Job.model_copy(job)

rich.print(job)
2026-06-27 22:09:01 DEBUG    | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db
Job(
    id=UUID('5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'),
    retry_count=0,
    updated_at=datetime.datetime(2026, 6, 28, 3, 9, 1, 351464),
    document_id=UUID('7102ad0c-dcc3-4164-a86f-4e601fe31e70'),
    status=<JobStatus.QUEUED: 'queued'>,
    created_at=datetime.datetime(2026, 6, 28, 3, 9, 1, 351451)
)
In [ ]:
async with get_session(settings=settings) as session:
    await process_queued_job(
        job,
        services=services,
        settings=settings,
        session=session,
    )
    rich.print(job)
2026-06-27 22:09:06 DEBUG    | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db
---------------------------------------------------------------------------
InvalidRequestError                       Traceback (most recent call last)
Cell In[8], line 2
      1 async with get_session(settings=settings) as session:
----> 2     await process_job(
      3         job,
      4         services=services,
      5         settings=settings,

File ~/Documents/transcription/src/transcription/worker.py:106, in process_job(job, services, settings, session)
    104     case _:
    105         return
--> 106 return await services.jobs.update_job(job, session=session)

File ~/Documents/transcription/src/transcription/services/jobs.py:50, in JobService.update_job(self, job, session)
     48 await _session.merge(job)
     49 await _session.commit()
---> 50 await _session.refresh(job)
     51 return job

File ~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/session.py:328, in AsyncSession.refresh(self, instance, attribute_names, with_for_update)
    308 async def refresh(
    309     self,
    310     instance: object,
    311     attribute_names: Optional[Iterable[str]] = None,
    312     with_for_update: ForUpdateParameter = None,
    313 ) -> None:
    314     """Expire and refresh the attributes on the given instance.
    315 
    316     A query will be issued to the database and all attributes will be
   (...)    325 
    326     """
--> 328     await greenlet_spawn(
    329         self.sync_session.refresh,
    330         instance,
    331         attribute_names=attribute_names,
    332         with_for_update=with_for_update,
    333     )

File ~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/util/_concurrency_py3k.py:190, in greenlet_spawn(fn, _require_await, *args, **kwargs)
    185 # runs the function synchronously in gl greenlet. If the execution
    186 # is interrupted by await_only, context is not dead and result is a
    187 # coroutine to wait. If the context is dead the function has
    188 # returned, and its result can be returned.
    189 switch_occurred = False
--> 190 result = context.switch(*args, **kwargs)
    191 while not context.dead:
    192     switch_occurred = True

File ~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py:3155, in Session.refresh(self, instance, attribute_names, with_for_update)
   3152 except exc.NO_STATE as err:
   3153     raise exc.UnmappedInstanceError(instance) from err
-> 3155 self._expire_state(state, attribute_names)
   3157 # this autoflush previously used to occur as a secondary effect
   3158 # of the load_on_ident below.   Meaning we'd organize the SELECT
   3159 # based on current DB pks, then flush, then if pks changed in that
   3160 # flush, crash.  this was unticketed but discovered as part of
   3161 # #8703.  So here, autoflush up front, dont autoflush inside
   3162 # load_on_ident.
   3163 self._autoflush()

File ~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py:3281, in Session._expire_state(self, state, attribute_names)
   3276 def _expire_state(
   3277     self,
   3278     state: InstanceState[Any],
   3279     attribute_names: Optional[Iterable[str]],
   3280 ) -> None:
-> 3281     self._validate_persistent(state)
   3282     if attribute_names:
   3283         state._expire_attributes(state.dict, attribute_names)

File ~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py:4139, in Session._validate_persistent(self, state)
   4137 def _validate_persistent(self, state: InstanceState[Any]) -> None:
   4138     if not self.identity_map.contains_state(state):
-> 4139         raise sa_exc.InvalidRequestError(
   4140             "Instance '%s' is not persistent within this Session"
   4141             % state_str(state)
   4142         )

InvalidRequestError: Instance '<Job at 0x7b19c4bb0b40>' is not persistent within this Session
In [ ]:
doc_path = Path("uploads/7c6ee648-4171-4e70-b901-ba94ee227d6a_Time Rolls On - page 013.jpg")
idx, og_name = doc_path.name.rpartition("_")[::2]
doc_id = UUID(idx)
doc = Document(id=doc_id, filename=og_name, file_path=str(doc_path))

document_service = DocumentService()
doc = await document_service.create_document(document=doc)
rich.print(doc)
In [ ]:
docs = await document_service.list_documents()
rich.print(docs)
In [ ]:
job = await services.jobs.read_job(UUID("6400c83d-e245-497c-902b-d8bc9db8a216"))
rich.print(job)
rich.print(job.document)
In [ ]:
rich.print(await services.jobs.list_jobs())
In [ ]:
rich.print(await document_service.list_documents())
In [ ]:
async with services.jobs._session_scope() as session:
    jobs = await services.jobs.list_jobs(session=session, load_docs=True)
    rich.print({job.id: job.document for job in jobs})
In [ ]:
job_service = JobService()
In [ ]:
doc_path.stem.rpartition("-")[0]
In [ ]:
async with get_session() as session:
    job = await services.jobs.read_job(job.id, session=session)
    rich.print(job.status)
    job.status = JobStatus.FAILED
    job.retry_count = 0
    await services.jobs.update_job(job, session=session)
    rich.print(job)
    await process_queued_job(job, services=services, session=session)
    rich.print(job)
In [ ]: