generated from john/python-template
model used being carried thru
This commit is contained in:
@@ -14,3 +14,8 @@ wheels/
|
||||
|
||||
# SQLite database
|
||||
*.db
|
||||
|
||||
upload/
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.png
|
||||
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"notebook.formatOnSave.enabled": true,
|
||||
"notebook.codeActionsOnSave": {
|
||||
"notebook.source.fixAll": "explicit",
|
||||
"notebook.source.organizeImports": "explicit"
|
||||
},
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "9bc5dd71",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%load_ext autoreload\n",
|
||||
"%autoreload 2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "4f4daf0f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"import rich\n",
|
||||
"from sqlmodel.ext.asyncio.session import AsyncSession\n",
|
||||
"\n",
|
||||
"from transcription.config import Settings\n",
|
||||
"from transcription.config import configure_logging\n",
|
||||
"from transcription.config import get_settings\n",
|
||||
"from transcription.db.operations import create_all\n",
|
||||
"from transcription.db.runtime import get_engine\n",
|
||||
"from transcription.db.runtime import get_session\n",
|
||||
"from transcription.models import Document\n",
|
||||
"from transcription.models import Job\n",
|
||||
"from transcription.services import ServiceBundle\n",
|
||||
"from transcription.services import workflows as wf\n",
|
||||
"from transcription.services.jobs import JobStatus\n",
|
||||
"from transcription.services.store import create_upload_job\n",
|
||||
"from transcription.services.transcription import transcribe_document_image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "c81d7c26",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"2026-06-28 12:53:12 DEBUG | Logging configured\n",
|
||||
"2026-06-28 12:53:12 DEBUG | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db\n",
|
||||
"2026-06-28 12:53:12 DEBUG | Database schema bootstrap complete for database_url=sqlite+aiosqlite:///./transcription.db\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"configure_logging()\n",
|
||||
"settings = get_settings(worker_max_retries=2)\n",
|
||||
"engine = get_engine(settings=settings)\n",
|
||||
"await create_all(engine=engine)\n",
|
||||
"s = ServiceBundle()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "7aca0ba3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'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'"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"settings.openrouter_api_key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ee1d5b0c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"test_file = Path(\"uploads/7c6ee648-4171-4e70-b901-ba94ee227d6a_Time Rolls On - page 013.jpg\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "90e14eca",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async def upload_file(file_path: str | Path, session: AsyncSession):\n",
|
||||
" path = Path(file_path)\n",
|
||||
" filename = path.name.rpartition(\"_\")[2]\n",
|
||||
" return await create_upload_job(\n",
|
||||
" filename=filename,\n",
|
||||
" file_bytes=path.read_bytes(),\n",
|
||||
" session=session,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async with get_session() as session:\n",
|
||||
" result = await upload_file(test_file, session)\n",
|
||||
"\n",
|
||||
"rich.print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "59413342",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"font-weight: bold\">[</span>\n",
|
||||
" <span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"font-weight: bold\">(</span>\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">document_id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'5c769d26-30bf-4950-ba3d-5dcabce690e7'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">status</span>=<span style=\"font-weight: bold\"><</span><span style=\"color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold\">JobStatus.QUEUED:</span><span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #008000; text-decoration-color: #008000\">'queued'</span><span style=\"font-weight: bold\">></span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">created_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">17</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">9</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">26</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">395914</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'7a0d5e44-64e9-4916-83e0-bde3a8fb8c58'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span>=<span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">17</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">47</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">26</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">755438</span><span style=\"font-weight: bold\">)</span>\n",
|
||||
" <span style=\"font-weight: bold\">)</span>\n",
|
||||
"<span style=\"font-weight: bold\">]</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[1m[\u001b[0m\n",
|
||||
" \u001b[1;35mJob\u001b[0m\u001b[1m(\u001b[0m\n",
|
||||
" \u001b[33mdocument_id\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'5c769d26-30bf-4950-ba3d-5dcabce690e7'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mstatus\u001b[0m=\u001b[1m<\u001b[0m\u001b[1;95mJobStatus.QUEUED:\u001b[0m\u001b[39m \u001b[0m\u001b[32m'queued'\u001b[0m\u001b[1m>\u001b[0m,\n",
|
||||
" \u001b[33mcreated_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m17\u001b[0m, \u001b[1;36m9\u001b[0m, \u001b[1;36m26\u001b[0m, \u001b[1;36m395914\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mid\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'7a0d5e44-64e9-4916-83e0-bde3a8fb8c58'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mretry_count\u001b[0m=\u001b[1;36m2\u001b[0m,\n",
|
||||
" \u001b[33mupdated_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m17\u001b[0m, \u001b[1;36m47\u001b[0m, \u001b[1;36m26\u001b[0m, \u001b[1;36m755438\u001b[0m\u001b[1m)\u001b[0m\n",
|
||||
" \u001b[1m)\u001b[0m\n",
|
||||
"\u001b[1m]\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Document</span><span style=\"font-weight: bold\">(</span>\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">uploaded_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">17</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">9</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">26</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">392233</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">file_path</span>=<span style=\"color: #008000; text-decoration-color: #008000\">'uploads/86a8f2c6-bfb7-4980-9941-56768902a9c0_Time Rolls On - page 013.jpg'</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">filename</span>=<span style=\"color: #008000; text-decoration-color: #008000\">'Time Rolls On - page 013.jpg'</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'5c769d26-30bf-4950-ba3d-5dcabce690e7'</span><span style=\"font-weight: bold\">)</span>\n",
|
||||
"<span style=\"font-weight: bold\">)</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[1;35mDocument\u001b[0m\u001b[1m(\u001b[0m\n",
|
||||
" \u001b[33muploaded_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m17\u001b[0m, \u001b[1;36m9\u001b[0m, \u001b[1;36m26\u001b[0m, \u001b[1;36m392233\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mfile_path\u001b[0m=\u001b[32m'uploads/86a8f2c6-bfb7-4980-9941-56768902a9c0_Time Rolls On - page 013.jpg'\u001b[0m,\n",
|
||||
" \u001b[33mfilename\u001b[0m=\u001b[32m'Time Rolls On - page 013.jpg'\u001b[0m,\n",
|
||||
" \u001b[33mid\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'5c769d26-30bf-4950-ba3d-5dcabce690e7'\u001b[0m\u001b[1m)\u001b[0m\n",
|
||||
"\u001b[1m)\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"jobs = await s.jobs.list_jobs()\n",
|
||||
"rich.print(jobs)\n",
|
||||
"document = jobs[0].document\n",
|
||||
"rich.print(document)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "d89041bb",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"2026-06-28 12:53:20 INFO | Loaded prompt artifact: prompts/transcribe_document.md\n",
|
||||
"2026-06-28 12:53:20 INFO | Starting transcription for image=uploads/86a8f2c6-bfb7-4980-9941-56768902a9c0_Time Rolls On - page 013.jpg mime_type=image/jpeg\n",
|
||||
"2026-06-28 12:53:22 INFO | HTTP Request: POST https://openrouter.ai/api/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
|
||||
"2026-06-28 12:53:26 INFO | OpenRouter transcription completed using model=google/gemini-2.5-flash\n",
|
||||
"2026-06-28 12:53:26 INFO | Transcription completed for image=uploads/86a8f2c6-bfb7-4980-9941-56768902a9c0_Time Rolls On - page 013.jpg provider=openrouter\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"TranscriptionResult(text='-13-\\nWe lived fairly well. We had plenty of wheat at the flour mill. We had\\n[taken this wheat as pay for our work in harvest. Beef was very cheap,\\nfour and five cents per pound, syrup one dollar per gallon in tin cans,\\nsugar five cents a pound, brown sugar, and my sister Texie was a good\\ncook, and Billy and Pleas would entertain us of an evening with their\\nwierd tales of their hardships in their travels to the railroad to\\nget work. They got $2.00 per day driving scraper teams on the dump.\\nThey had to go to Sprague, a distance of 60 miles, and carry their\\nblankets on their backs. Billy always carried a pistol and Pleas a\\npipe. They were pretty short of change and finally went broke. This\\ntook place near Rock Lake, Washington Territory. So Billy traded his\\npistol for two loaves of bread and supper and breakfast for both of\\nthem. But before they got a job Pleas had to trade his pipe for a\\ncouple more loaves.\\n\\nNow this winter of 1880 was a long and dreary winter. There was lots\\nof snow and we did not go to any of the social activities such as church\\nor debates at the school house, or singings in the neighborhood, and\\nTexie and I named the entertainments given from time to time each\\nevening \"The Modoc Struggle.\" Until this day the phrase used in the\\nCochran or Kittrell families expresses some memorable hardships in the\\nbattle of life.\\n\\nIn February of 1881 I visited our old friends, the Stevensons, and Mr.\\nStevenson was wanting a man to help him cut some cordwood and would\\npay one dollar per day. Snow! Snow! Snow! Everyday I was expecting\\nhim to have to quit work. Mr. Stevenson told me afterward he was expect-\\ning me to quit anytime on account of the snow and cold and he was anxious\\nto get enough wood cut to give him employment hauling wood to Colfax\\nduring the summer to get his groceries and pay his store bill, as credit\\nwas all the go with the homesteader.\\n\\nCapital stood in with men who were trying to improve the land and make\\na home. This credit system worked, especially for the merchant and the\\nimplement concerns and fairly well for the banker. The common interest\\nwas 1½ percent per month, and sometimes as high as two percent. There\\nwere many stock mortgages or a man on your note. The notes were usually\\npayable in 90 days or renew your note.\\n\\nSo the spring of 1881 was here and the O.R. and N. railroad was being\\nbuilt from Texas Ferry on the Snake River to Colfax. Men teams were\\nwanted at $4.00 per day. Pleas, Texie, Billy and I were anxious to\\nstart. We bought us a tent and a team apiece and went to Texas Ferry\\nto make our fortune. The roads were not very good. The horses were\\nrather cold shouldered and we had to be pulled out of the mud sometimes\\nbut teamsters were good hearted and seldom charged each other. This\\nwas March. When we go[t] to Snake River it was fine spring weather.\\n\\nNow it was December 1880 that Billy and I had come from Walla Walla in\\na storm of snow and hundreds of head of cattle were in the road. They\\nwere lowing and taking shelter along the bluffs. Now as we were going\\nalong the same road for miles a person could step from one [carcass?] to\\nanother. There were carcasses of cows, calves, big steers, and some\\nhorses, lining the road.\\n\\nThe Company had to clear the way for travel. Stock men had prepared\\nno feed. Now they had no stock. It was referred to as the \"Cow Killer\"', provider='openrouter', prompt_name='', model='google/gemini-2.5-flash')"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"await transcribe_document_image(document.file_path)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "b5e66d40",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async with get_session() as session:\n",
|
||||
" queued_jobs = await s.jobs.query_jobs(status=JobStatus.QUEUED, session=session)\n",
|
||||
" job = await s.jobs.read_job(queued_jobs[0].id, session=session)\n",
|
||||
"\n",
|
||||
"rich.print(job)\n",
|
||||
"job = Job.model_copy(job)\n",
|
||||
"rich.print(job.document)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "b8695009",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"2026-06-28 12:53:53 DEBUG | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db\n",
|
||||
"2026-06-28 12:53:53 INFO | Loaded prompt artifact: prompts/transcribe_document.md\n",
|
||||
"2026-06-28 12:53:53 INFO | Starting transcription for image=uploads/86a8f2c6-bfb7-4980-9941-56768902a9c0_Time Rolls On - page 013.jpg mime_type=image/jpeg\n",
|
||||
"2026-06-28 12:53:54 INFO | HTTP Request: POST https://openrouter.ai/api/v1/chat/completions \"HTTP/1.1 200 OK\"\n",
|
||||
"2026-06-28 12:53:58 INFO | OpenRouter transcription completed using model=google/gemini-2.5-flash\n",
|
||||
"2026-06-28 12:53:58 INFO | Transcription completed for image=uploads/86a8f2c6-bfb7-4980-9941-56768902a9c0_Time Rolls On - page 013.jpg provider=openrouter\n",
|
||||
"2026-06-28 12:53:58 INFO | Job transcribed operation=worker.process_job job_id=7a0d5e44-64e9-4916-83e0-bde3a8fb8c58 document_id=5c769d26-30bf-4950-ba3d-5dcabce690e7 provider=openrouter\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"font-weight: bold\">(</span>\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">document_id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'5c769d26-30bf-4950-ba3d-5dcabce690e7'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">status</span>=<span style=\"font-weight: bold\"><</span><span style=\"color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold\">JobStatus.TRANSCRIBED:</span><span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #008000; text-decoration-color: #008000\">'transcribed'</span><span style=\"font-weight: bold\">></span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">created_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">17</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">9</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">26</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">395914</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'7a0d5e44-64e9-4916-83e0-bde3a8fb8c58'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span>=<span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">17</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">53</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">58</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">96639</span><span style=\"font-weight: bold\">)</span>\n",
|
||||
"<span style=\"font-weight: bold\">)</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[1;35mJob\u001b[0m\u001b[1m(\u001b[0m\n",
|
||||
" \u001b[33mdocument_id\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'5c769d26-30bf-4950-ba3d-5dcabce690e7'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mstatus\u001b[0m=\u001b[1m<\u001b[0m\u001b[1;95mJobStatus.TRANSCRIBED:\u001b[0m\u001b[39m \u001b[0m\u001b[32m'transcribed'\u001b[0m\u001b[1m>\u001b[0m,\n",
|
||||
" \u001b[33mcreated_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m17\u001b[0m, \u001b[1;36m9\u001b[0m, \u001b[1;36m26\u001b[0m, \u001b[1;36m395914\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mid\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'7a0d5e44-64e9-4916-83e0-bde3a8fb8c58'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mretry_count\u001b[0m=\u001b[1;36m2\u001b[0m,\n",
|
||||
" \u001b[33mupdated_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m17\u001b[0m, \u001b[1;36m53\u001b[0m, \u001b[1;36m58\u001b[0m, \u001b[1;36m96639\u001b[0m\u001b[1m)\u001b[0m\n",
|
||||
"\u001b[1m)\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"async with get_session() as session:\n",
|
||||
" queued_jobs = await s.jobs.list_jobs(session=session)\n",
|
||||
" job = await s.jobs.read_job(queued_jobs[0].id, session=session)\n",
|
||||
"\n",
|
||||
"result = await wf.advance_job(\n",
|
||||
" job,\n",
|
||||
" services=s,\n",
|
||||
" settings=settings,\n",
|
||||
" # session=session,\n",
|
||||
")\n",
|
||||
"rich.print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "5b767e7f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"rich.print(settings)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a140db21",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async def reset_jobs():\n",
|
||||
" \"\"\"Reset all jobs to QUEUED for testing purposes.\"\"\"\n",
|
||||
" async with get_session() as session:\n",
|
||||
" jobs = await s.jobs.query_jobs(session=session)\n",
|
||||
" for job in jobs:\n",
|
||||
" job.status = JobStatus.QUEUED\n",
|
||||
" await s.jobs.update_job(job, session=session)\n",
|
||||
" await session.commit()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"await reset_jobs()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "477aaa2a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "transcription (3.12.11)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.11"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "c2b3a63a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%load_ext autoreload\n",
|
||||
"%autoreload 2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a84d3520",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"2026-06-27 22:09:01 DEBUG | Logging configured\n",
|
||||
"2026-06-27 22:09:01 DEBUG | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db\n",
|
||||
"2026-06-27 22:09:01 DEBUG | Database schema bootstrap complete for database_url=sqlite+aiosqlite:///./transcription.db\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from pathlib import Path\n",
|
||||
"from uuid import UUID\n",
|
||||
"\n",
|
||||
"import rich\n",
|
||||
"\n",
|
||||
"from transcription.config import configure_logging\n",
|
||||
"from transcription.config import get_settings\n",
|
||||
"from transcription.db.operations import create_all\n",
|
||||
"from transcription.db.runtime import get_engine\n",
|
||||
"from transcription.db.runtime import get_session\n",
|
||||
"from transcription.models import Document\n",
|
||||
"from transcription.models import Job\n",
|
||||
"from transcription.models import JobStatus\n",
|
||||
"from transcription.services import ServiceBundle\n",
|
||||
"from transcription.services.documents import DocumentService\n",
|
||||
"from transcription.services.documents import create_upload_job\n",
|
||||
"from transcription.services.jobs import JobService\n",
|
||||
"from transcription.services.store import store_file\n",
|
||||
"from transcription.worker import process_queued_job\n",
|
||||
"\n",
|
||||
"configure_logging()\n",
|
||||
"settings = get_settings(worker_max_retries=2)\n",
|
||||
"engine = get_engine(settings=settings)\n",
|
||||
"await create_all(engine=engine)\n",
|
||||
"services = ServiceBundle()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "b3b2c783",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"font-weight: bold\">[</span>\n",
|
||||
" <span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"font-weight: bold\">(</span>\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'c79d20d1-ab5c-4b3b-a898-13eac7d2c71a'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span>=<span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">0</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">55</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">13</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">69063</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">document_id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'105e84b2-d773-4abb-bd49-8dc26b966046'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">status</span>=<span style=\"font-weight: bold\"><</span><span style=\"color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold\">JobStatus.TRANSCRIBED:</span><span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #008000; text-decoration-color: #008000\">'transcribed'</span><span style=\"color: #000000; text-decoration-color: #000000\">>,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">created_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">55</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">13</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">69054</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'271b688e-63c7-4652-9ccf-5c1dd3f61058'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">0</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">55</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">17</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">189346</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">document_id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'06f49121-9b98-4b0e-b879-7a3b959fe984'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">status</span><span style=\"color: #000000; text-decoration-color: #000000\">=<JobStatus.TRANSCRIBED: </span><span style=\"color: #008000; text-decoration-color: #008000\">'transcribed'</span><span style=\"color: #000000; text-decoration-color: #000000\">>,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">created_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">55</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">17</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">189338</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'4386391c-3ce9-412a-9bd4-46b3860d0ef7'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">0</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">55</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">19</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">161570</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">document_id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'ac04c024-25ca-4707-8cd8-641606976344'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">status</span><span style=\"color: #000000; text-decoration-color: #000000\">=<JobStatus.TRANSCRIBED: </span><span style=\"color: #008000; text-decoration-color: #008000\">'transcribed'</span><span style=\"color: #000000; text-decoration-color: #000000\">>,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">created_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">55</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">19</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">161560</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'6637b36d-82e1-41e1-8776-037183851d30'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">0</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">55</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">26</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">921982</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">document_id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'9c6d8632-fe6b-4f70-bb9b-5e8d762b4b3e'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">status</span><span style=\"color: #000000; text-decoration-color: #000000\">=<JobStatus.TRANSCRIBED: </span><span style=\"color: #008000; text-decoration-color: #008000\">'transcribed'</span><span style=\"color: #000000; text-decoration-color: #000000\">>,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">created_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">55</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">26</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">921974</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'758f457a-343c-4b4a-98af-278875303803'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">0</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">3</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">4</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">40</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">999438</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">document_id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'0fb70d21-6409-40cc-83df-abce2917f367'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">status</span><span style=\"color: #000000; text-decoration-color: #000000\">=<JobStatus.TRANSCRIBED: </span><span style=\"color: #008000; text-decoration-color: #008000\">'transcribed'</span><span style=\"color: #000000; text-decoration-color: #000000\">>,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">created_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">3</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">4</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">40</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">999427</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'01060a33-464d-48c9-b4bb-f1f6acd91820'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">0</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">3</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">7</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">284063</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">document_id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'c8d0eacb-87dd-4d88-a576-ce810436dc45'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">status</span><span style=\"color: #000000; text-decoration-color: #000000\">=<JobStatus.TRANSCRIBED: </span><span style=\"color: #008000; text-decoration-color: #008000\">'transcribed'</span><span style=\"color: #000000; text-decoration-color: #000000\">>,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">created_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">3</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">7</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">284053</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'50800d67-fece-452e-b3ff-b7045fb1838f'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">0</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">3</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">8</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span><span style=\"color: #000000; text-decoration-color: #000000\">, </span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">737235</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">document_id</span><span style=\"color: #000000; text-decoration-color: #000000\">=</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'273a58fd-4c4d-4e3a-93dd-4f391650e646'</span><span style=\"color: #000000; text-decoration-color: #000000; font-weight: bold\">)</span><span style=\"color: #000000; text-decoration-color: #000000\">,</span>\n",
|
||||
"<span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #808000; text-decoration-color: #808000\">status</span><span style=\"color: #000000; text-decoration-color: #000000\">=<JobStatus.TRANSCRIBED: </span><span style=\"color: #008000; text-decoration-color: #008000\">'transcribed'</span><span style=\"font-weight: bold\">></span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">created_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">3</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">8</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">737226</span><span style=\"font-weight: bold\">)</span>\n",
|
||||
" <span style=\"font-weight: bold\">)</span>\n",
|
||||
"<span style=\"font-weight: bold\">]</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[1m[\u001b[0m\n",
|
||||
" \u001b[1;35mJob\u001b[0m\u001b[1m(\u001b[0m\n",
|
||||
" \u001b[33mid\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'c79d20d1-ab5c-4b3b-a898-13eac7d2c71a'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mretry_count\u001b[0m=\u001b[1;36m0\u001b[0m,\n",
|
||||
" \u001b[33mupdated_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m55\u001b[0m, \u001b[1;36m13\u001b[0m, \u001b[1;36m69063\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mdocument_id\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'105e84b2-d773-4abb-bd49-8dc26b966046'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mstatus\u001b[0m=\u001b[1m<\u001b[0m\u001b[1;95mJobStatus.TRANSCRIBED:\u001b[0m\u001b[39m \u001b[0m\u001b[32m'transcribed'\u001b[0m\u001b[39m>,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mcreated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m2\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m55\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m13\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m69054\u001b[0m\u001b[1;39m)\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;35mJob\u001b[0m\u001b[1;39m(\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mid\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'271b688e-63c7-4652-9ccf-5c1dd3f61058'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mretry_count\u001b[0m\u001b[39m=\u001b[0m\u001b[1;36m0\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mupdated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m2\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m55\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m17\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m189346\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mdocument_id\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'06f49121-9b98-4b0e-b879-7a3b959fe984'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mstatus\u001b[0m\u001b[39m=<JobStatus.TRANSCRIBED: \u001b[0m\u001b[32m'transcribed'\u001b[0m\u001b[39m>,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mcreated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m2\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m55\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m17\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m189338\u001b[0m\u001b[1;39m)\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;35mJob\u001b[0m\u001b[1;39m(\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mid\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'4386391c-3ce9-412a-9bd4-46b3860d0ef7'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mretry_count\u001b[0m\u001b[39m=\u001b[0m\u001b[1;36m0\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mupdated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m2\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m55\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m19\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m161570\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mdocument_id\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'ac04c024-25ca-4707-8cd8-641606976344'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mstatus\u001b[0m\u001b[39m=<JobStatus.TRANSCRIBED: \u001b[0m\u001b[32m'transcribed'\u001b[0m\u001b[39m>,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mcreated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m2\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m55\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m19\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m161560\u001b[0m\u001b[1;39m)\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;35mJob\u001b[0m\u001b[1;39m(\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mid\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'6637b36d-82e1-41e1-8776-037183851d30'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mretry_count\u001b[0m\u001b[39m=\u001b[0m\u001b[1;36m0\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mupdated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m2\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m55\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m26\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m921982\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mdocument_id\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'9c6d8632-fe6b-4f70-bb9b-5e8d762b4b3e'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mstatus\u001b[0m\u001b[39m=<JobStatus.TRANSCRIBED: \u001b[0m\u001b[32m'transcribed'\u001b[0m\u001b[39m>,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mcreated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m2\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m55\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m26\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m921974\u001b[0m\u001b[1;39m)\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;35mJob\u001b[0m\u001b[1;39m(\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mid\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'758f457a-343c-4b4a-98af-278875303803'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mretry_count\u001b[0m\u001b[39m=\u001b[0m\u001b[1;36m0\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mupdated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m3\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m4\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m40\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m999438\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mdocument_id\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'0fb70d21-6409-40cc-83df-abce2917f367'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mstatus\u001b[0m\u001b[39m=<JobStatus.TRANSCRIBED: \u001b[0m\u001b[32m'transcribed'\u001b[0m\u001b[39m>,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mcreated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m3\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m4\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m40\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m999427\u001b[0m\u001b[1;39m)\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;35mJob\u001b[0m\u001b[1;39m(\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mid\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'01060a33-464d-48c9-b4bb-f1f6acd91820'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mretry_count\u001b[0m\u001b[39m=\u001b[0m\u001b[1;36m0\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mupdated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m3\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m7\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m284063\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mdocument_id\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'c8d0eacb-87dd-4d88-a576-ce810436dc45'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mstatus\u001b[0m\u001b[39m=<JobStatus.TRANSCRIBED: \u001b[0m\u001b[32m'transcribed'\u001b[0m\u001b[39m>,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mcreated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m3\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m7\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m284053\u001b[0m\u001b[1;39m)\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[1;35mJob\u001b[0m\u001b[1;39m(\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mid\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'50800d67-fece-452e-b3ff-b7045fb1838f'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mretry_count\u001b[0m\u001b[39m=\u001b[0m\u001b[1;36m0\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mupdated_at\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1;39m(\u001b[0m\u001b[1;36m2026\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m6\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m28\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m3\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m8\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m2\u001b[0m\u001b[39m, \u001b[0m\u001b[1;36m737235\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mdocument_id\u001b[0m\u001b[39m=\u001b[0m\u001b[1;35mUUID\u001b[0m\u001b[1;39m(\u001b[0m\u001b[32m'273a58fd-4c4d-4e3a-93dd-4f391650e646'\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m,\u001b[0m\n",
|
||||
"\u001b[39m \u001b[0m\u001b[33mstatus\u001b[0m\u001b[39m=<JobStatus.TRANSCRIBED: \u001b[0m\u001b[32m'transcribed'\u001b[0m\u001b[1m>\u001b[0m,\n",
|
||||
" \u001b[33mcreated_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m3\u001b[0m, \u001b[1;36m8\u001b[0m, \u001b[1;36m2\u001b[0m, \u001b[1;36m737226\u001b[0m\u001b[1m)\u001b[0m\n",
|
||||
" \u001b[1m)\u001b[0m\n",
|
||||
"\u001b[1m]\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"jobs = await services.jobs.list_jobs()\n",
|
||||
"rich.print(jobs)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "60788806",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'Time Rolls On - page 013.jpg'"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"doc_path = Path(\"uploads/7c6ee648-4171-4e70-b901-ba94ee227d6a_Time Rolls On - page 013.jpg\")\n",
|
||||
"doc_path.name.rpartition(\"_\")[2]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "63881a49",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"2026-06-27 22:09:01 DEBUG | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db\n",
|
||||
"2026-06-27 22:09:01 INFO | Stored uploaded file: uploads/9d3ce411-e027-4e97-9c11-786783926ce4_Time Rolls On - page 013.jpg\n",
|
||||
"2026-06-27 22:09:01 INFO | Created upload job document_id=7102ad0c-dcc3-4164-a86f-4e601fe31e70 job_id=5bb45fc1-6b3b-4e92-9942-f8f059baaaaf\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UploadJobResult</span><span style=\"font-weight: bold\">(</span>\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">document_id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'7102ad0c-dcc3-4164-a86f-4e601fe31e70'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">job_id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">stored_path</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">PosixPath</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'uploads/9d3ce411-e027-4e97-9c11-786783926ce4_Time Rolls On - page 013.jpg'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">original_filename</span>=<span style=\"color: #008000; text-decoration-color: #008000\">'Time Rolls On - page 013.jpg'</span>\n",
|
||||
"<span style=\"font-weight: bold\">)</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[1;35mUploadJobResult\u001b[0m\u001b[1m(\u001b[0m\n",
|
||||
" \u001b[33mdocument_id\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'7102ad0c-dcc3-4164-a86f-4e601fe31e70'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mjob_id\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mstored_path\u001b[0m=\u001b[1;35mPosixPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'uploads/9d3ce411-e027-4e97-9c11-786783926ce4_Time Rolls On - page 013.jpg'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33moriginal_filename\u001b[0m=\u001b[32m'Time Rolls On - page 013.jpg'\u001b[0m\n",
|
||||
"\u001b[1m)\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"async with get_session(settings=settings) as session:\n",
|
||||
" result = await create_upload_job(\n",
|
||||
" session=session,\n",
|
||||
" filename=doc_path.name.rpartition(\"_\")[2],\n",
|
||||
" file_bytes=doc_path.read_bytes(),\n",
|
||||
" settings=settings,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"rich.print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "c3d92127",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"font-weight: bold\">(</span>\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span>=<span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">0</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">3</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">9</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">1</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">351464</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">document_id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'7102ad0c-dcc3-4164-a86f-4e601fe31e70'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">status</span>=<span style=\"font-weight: bold\"><</span><span style=\"color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold\">JobStatus.QUEUED:</span><span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #008000; text-decoration-color: #008000\">'queued'</span><span style=\"font-weight: bold\">></span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">created_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">3</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">9</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">1</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">351451</span><span style=\"font-weight: bold\">)</span>\n",
|
||||
"<span style=\"font-weight: bold\">)</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[1;35mJob\u001b[0m\u001b[1m(\u001b[0m\n",
|
||||
" \u001b[33mid\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mretry_count\u001b[0m=\u001b[1;36m0\u001b[0m,\n",
|
||||
" \u001b[33mupdated_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m3\u001b[0m, \u001b[1;36m9\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m351464\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mdocument_id\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'7102ad0c-dcc3-4164-a86f-4e601fe31e70'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mstatus\u001b[0m=\u001b[1m<\u001b[0m\u001b[1;95mJobStatus.QUEUED:\u001b[0m\u001b[39m \u001b[0m\u001b[32m'queued'\u001b[0m\u001b[1m>\u001b[0m,\n",
|
||||
" \u001b[33mcreated_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m3\u001b[0m, \u001b[1;36m9\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m351451\u001b[0m\u001b[1m)\u001b[0m\n",
|
||||
"\u001b[1m)\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import random\n",
|
||||
"\n",
|
||||
"job = random.choice(await services.jobs.query_jobs(status=JobStatus.QUEUED))\n",
|
||||
"rich.print(job)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "5209430c",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"2026-06-27 22:09:01 DEBUG | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">Job</span><span style=\"font-weight: bold\">(</span>\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">retry_count</span>=<span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">0</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">updated_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">3</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">9</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">1</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">351464</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">document_id</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">UUID</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008000; text-decoration-color: #008000\">'7102ad0c-dcc3-4164-a86f-4e601fe31e70'</span><span style=\"font-weight: bold\">)</span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">status</span>=<span style=\"font-weight: bold\"><</span><span style=\"color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold\">JobStatus.QUEUED:</span><span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #008000; text-decoration-color: #008000\">'queued'</span><span style=\"font-weight: bold\">></span>,\n",
|
||||
" <span style=\"color: #808000; text-decoration-color: #808000\">created_at</span>=<span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">datetime</span><span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">.datetime</span><span style=\"font-weight: bold\">(</span><span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">2026</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">6</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">28</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">3</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">9</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">1</span>, <span style=\"color: #008080; text-decoration-color: #008080; font-weight: bold\">351451</span><span style=\"font-weight: bold\">)</span>\n",
|
||||
"<span style=\"font-weight: bold\">)</span>\n",
|
||||
"</pre>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"\u001b[1;35mJob\u001b[0m\u001b[1m(\u001b[0m\n",
|
||||
" \u001b[33mid\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'5bb45fc1-6b3b-4e92-9942-f8f059baaaaf'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mretry_count\u001b[0m=\u001b[1;36m0\u001b[0m,\n",
|
||||
" \u001b[33mupdated_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m3\u001b[0m, \u001b[1;36m9\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m351464\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mdocument_id\u001b[0m=\u001b[1;35mUUID\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'7102ad0c-dcc3-4164-a86f-4e601fe31e70'\u001b[0m\u001b[1m)\u001b[0m,\n",
|
||||
" \u001b[33mstatus\u001b[0m=\u001b[1m<\u001b[0m\u001b[1;95mJobStatus.QUEUED:\u001b[0m\u001b[39m \u001b[0m\u001b[32m'queued'\u001b[0m\u001b[1m>\u001b[0m,\n",
|
||||
" \u001b[33mcreated_at\u001b[0m=\u001b[1;35mdatetime\u001b[0m\u001b[1;35m.datetime\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m2026\u001b[0m, \u001b[1;36m6\u001b[0m, \u001b[1;36m28\u001b[0m, \u001b[1;36m3\u001b[0m, \u001b[1;36m9\u001b[0m, \u001b[1;36m1\u001b[0m, \u001b[1;36m351451\u001b[0m\u001b[1m)\u001b[0m\n",
|
||||
"\u001b[1m)\u001b[0m\n"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"async with get_session(settings=settings) as session:\n",
|
||||
" jobs = await services.jobs.query_jobs(\n",
|
||||
" status=JobStatus.QUEUED,\n",
|
||||
" session=session,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" job = random.choice(jobs)\n",
|
||||
" job = Job.model_copy(job)\n",
|
||||
"\n",
|
||||
"rich.print(job)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "569e5d9f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"2026-06-27 22:09:06 DEBUG | Initialized async database runtime for database_url=sqlite+aiosqlite:///./transcription.db\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"ename": "InvalidRequestError",
|
||||
"evalue": "Instance '<Job at 0x7b19c4bb0b40>' is not persistent within this Session",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
||||
"\u001b[31mInvalidRequestError\u001b[39m Traceback (most recent call last)",
|
||||
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;28;01masync\u001b[39;00m \u001b[38;5;28;01mwith\u001b[39;00m get_session(settings=settings) \u001b[38;5;28;01mas\u001b[39;00m session:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m await process_job(\n\u001b[32m 3\u001b[39m job,\n\u001b[32m 4\u001b[39m services=services,\n\u001b[32m 5\u001b[39m settings=settings,\n",
|
||||
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/transcription/src/transcription/worker.py:106\u001b[39m, in \u001b[36mprocess_job\u001b[39m\u001b[34m(job, services, settings, session)\u001b[39m\n\u001b[32m 104\u001b[39m \u001b[38;5;28;01mcase\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01m_\u001b[39;00m:\n\u001b[32m 105\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m106\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mawait\u001b[39;00m services.jobs.update_job(job, session=session)\n",
|
||||
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/transcription/src/transcription/services/jobs.py:50\u001b[39m, in \u001b[36mJobService.update_job\u001b[39m\u001b[34m(self, job, session)\u001b[39m\n\u001b[32m 48\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m _session.merge(job)\n\u001b[32m 49\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m _session.commit()\n\u001b[32m---> \u001b[39m\u001b[32m50\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m _session.refresh(job)\n\u001b[32m 51\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m job\n",
|
||||
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/session.py:328\u001b[39m, in \u001b[36mAsyncSession.refresh\u001b[39m\u001b[34m(self, instance, attribute_names, with_for_update)\u001b[39m\n\u001b[32m 308\u001b[39m \u001b[38;5;28;01masync\u001b[39;00m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mrefresh\u001b[39m(\n\u001b[32m 309\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m 310\u001b[39m instance: \u001b[38;5;28mobject\u001b[39m,\n\u001b[32m 311\u001b[39m attribute_names: Optional[Iterable[\u001b[38;5;28mstr\u001b[39m]] = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 312\u001b[39m with_for_update: ForUpdateParameter = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 313\u001b[39m ) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 314\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Expire and refresh the attributes on the given instance.\u001b[39;00m\n\u001b[32m 315\u001b[39m \n\u001b[32m 316\u001b[39m \u001b[33;03m A query will be issued to the database and all attributes will be\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 325\u001b[39m \n\u001b[32m 326\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m328\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m greenlet_spawn(\n\u001b[32m 329\u001b[39m \u001b[38;5;28mself\u001b[39m.sync_session.refresh,\n\u001b[32m 330\u001b[39m instance,\n\u001b[32m 331\u001b[39m attribute_names=attribute_names,\n\u001b[32m 332\u001b[39m with_for_update=with_for_update,\n\u001b[32m 333\u001b[39m )\n",
|
||||
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/util/_concurrency_py3k.py:190\u001b[39m, in \u001b[36mgreenlet_spawn\u001b[39m\u001b[34m(fn, _require_await, *args, **kwargs)\u001b[39m\n\u001b[32m 185\u001b[39m \u001b[38;5;66;03m# runs the function synchronously in gl greenlet. If the execution\u001b[39;00m\n\u001b[32m 186\u001b[39m \u001b[38;5;66;03m# is interrupted by await_only, context is not dead and result is a\u001b[39;00m\n\u001b[32m 187\u001b[39m \u001b[38;5;66;03m# coroutine to wait. If the context is dead the function has\u001b[39;00m\n\u001b[32m 188\u001b[39m \u001b[38;5;66;03m# returned, and its result can be returned.\u001b[39;00m\n\u001b[32m 189\u001b[39m switch_occurred = \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m190\u001b[39m result = \u001b[30;43mcontext\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mswitch\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43margs\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mkwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 191\u001b[39m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m context.dead:\n\u001b[32m 192\u001b[39m switch_occurred = \u001b[38;5;28;01mTrue\u001b[39;00m\n",
|
||||
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py:3155\u001b[39m, in \u001b[36mSession.refresh\u001b[39m\u001b[34m(self, instance, attribute_names, with_for_update)\u001b[39m\n\u001b[32m 3152\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m exc.NO_STATE \u001b[38;5;28;01mas\u001b[39;00m err:\n\u001b[32m 3153\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m exc.UnmappedInstanceError(instance) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01merr\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m3155\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_expire_state\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mstate\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mattribute_names\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 3157\u001b[39m \u001b[38;5;66;03m# this autoflush previously used to occur as a secondary effect\u001b[39;00m\n\u001b[32m 3158\u001b[39m \u001b[38;5;66;03m# of the load_on_ident below. Meaning we'd organize the SELECT\u001b[39;00m\n\u001b[32m 3159\u001b[39m \u001b[38;5;66;03m# based on current DB pks, then flush, then if pks changed in that\u001b[39;00m\n\u001b[32m 3160\u001b[39m \u001b[38;5;66;03m# flush, crash. this was unticketed but discovered as part of\u001b[39;00m\n\u001b[32m 3161\u001b[39m \u001b[38;5;66;03m# #8703. So here, autoflush up front, dont autoflush inside\u001b[39;00m\n\u001b[32m 3162\u001b[39m \u001b[38;5;66;03m# load_on_ident.\u001b[39;00m\n\u001b[32m 3163\u001b[39m \u001b[38;5;28mself\u001b[39m._autoflush()\n",
|
||||
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py:3281\u001b[39m, in \u001b[36mSession._expire_state\u001b[39m\u001b[34m(self, state, attribute_names)\u001b[39m\n\u001b[32m 3276\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_expire_state\u001b[39m(\n\u001b[32m 3277\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m 3278\u001b[39m state: InstanceState[Any],\n\u001b[32m 3279\u001b[39m attribute_names: Optional[Iterable[\u001b[38;5;28mstr\u001b[39m]],\n\u001b[32m 3280\u001b[39m ) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m3281\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_validate_persistent\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mstate\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 3282\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m attribute_names:\n\u001b[32m 3283\u001b[39m state._expire_attributes(state.dict, attribute_names)\n",
|
||||
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/transcription/.venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py:4139\u001b[39m, in \u001b[36mSession._validate_persistent\u001b[39m\u001b[34m(self, state)\u001b[39m\n\u001b[32m 4137\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_validate_persistent\u001b[39m(\u001b[38;5;28mself\u001b[39m, state: InstanceState[Any]) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 4138\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m.identity_map.contains_state(state):\n\u001b[32m-> \u001b[39m\u001b[32m4139\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m sa_exc.InvalidRequestError(\n\u001b[32m 4140\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mInstance \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m is not persistent within this Session\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 4141\u001b[39m % state_str(state)\n\u001b[32m 4142\u001b[39m )\n",
|
||||
"\u001b[31mInvalidRequestError\u001b[39m: Instance '<Job at 0x7b19c4bb0b40>' is not persistent within this Session"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"async with get_session(settings=settings) as session:\n",
|
||||
" await process_queued_job(\n",
|
||||
" job,\n",
|
||||
" services=services,\n",
|
||||
" settings=settings,\n",
|
||||
" session=session,\n",
|
||||
" )\n",
|
||||
" rich.print(job)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a5c06eaa",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"doc_path = Path(\"uploads/7c6ee648-4171-4e70-b901-ba94ee227d6a_Time Rolls On - page 013.jpg\")\n",
|
||||
"idx, og_name = doc_path.name.rpartition(\"_\")[::2]\n",
|
||||
"doc_id = UUID(idx)\n",
|
||||
"doc = Document(id=doc_id, filename=og_name, file_path=str(doc_path))\n",
|
||||
"\n",
|
||||
"document_service = DocumentService()\n",
|
||||
"doc = await document_service.create_document(document=doc)\n",
|
||||
"rich.print(doc)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "58d68d4e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"docs = await document_service.list_documents()\n",
|
||||
"rich.print(docs)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "35a06ab1",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"job = await services.jobs.read_job(UUID(\"6400c83d-e245-497c-902b-d8bc9db8a216\"))\n",
|
||||
"rich.print(job)\n",
|
||||
"rich.print(job.document)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "0d892d74",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"rich.print(await services.jobs.list_jobs())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "9b17fd22",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"rich.print(await document_service.list_documents())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d14be1d9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async with services.jobs._session_scope() as session:\n",
|
||||
" jobs = await services.jobs.list_jobs(session=session, load_docs=True)\n",
|
||||
" rich.print({job.id: job.document for job in jobs})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3845c342",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"job_service = JobService()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8889d812",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"doc_path.stem.rpartition(\"-\")[0]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "258a97be",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"async with get_session() as session:\n",
|
||||
" job = await services.jobs.read_job(job.id, session=session)\n",
|
||||
" rich.print(job.status)\n",
|
||||
" job.status = JobStatus.FAILED\n",
|
||||
" job.retry_count = 0\n",
|
||||
" await services.jobs.update_job(job, session=session)\n",
|
||||
" rich.print(job)\n",
|
||||
" await process_queued_job(job, services=services, session=session)\n",
|
||||
" rich.print(job)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ff66c87c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "transcription (3.12.11)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.11"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
from nicegui import ui
|
||||
|
||||
from transcription.app import create_app
|
||||
from transcription.config import configure_logging
|
||||
|
||||
configure_logging()
|
||||
|
||||
app = create_app()
|
||||
ui.run_with(
|
||||
app,
|
||||
mount_path="/ui",
|
||||
show_welcome_message=True,
|
||||
)
|
||||
ui.run(host="0.0.0.0", port=8085, reload=True)
|
||||
@@ -51,10 +51,15 @@ def _ensure_sqlite_compat_columns(connection: Connection) -> None:
|
||||
|
||||
inspector = inspect(connection)
|
||||
table_names = set(inspector.get_table_names())
|
||||
if "job" not in table_names:
|
||||
return
|
||||
|
||||
columns = {column["name"] for column in inspector.get_columns("job")}
|
||||
if "retry_count" not in columns:
|
||||
if "job" in table_names:
|
||||
job_columns = {column["name"] for column in inspector.get_columns("job")}
|
||||
if "retry_count" not in job_columns:
|
||||
connection.execute(text("ALTER TABLE job ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0"))
|
||||
logger.warning("Applied SQLite compatibility schema patch table=job column=retry_count default=0")
|
||||
|
||||
if "transcript" in table_names:
|
||||
transcript_columns = {column["name"] for column in inspector.get_columns("transcript")}
|
||||
if "model" not in transcript_columns:
|
||||
connection.execute(text("ALTER TABLE transcript ADD COLUMN model VARCHAR NOT NULL DEFAULT 'unknown'"))
|
||||
logger.warning("Applied SQLite compatibility schema patch table=transcript column=model default=unknown")
|
||||
|
||||
@@ -65,6 +65,8 @@ class Transcript(SQLModel, table=True):
|
||||
"""Revision number for this job's transcript history, starting at 0."""
|
||||
provider: str
|
||||
"""Name of the transcription provider used to generate this transcript."""
|
||||
model: str
|
||||
"""Model identifier used to generate this transcript revision."""
|
||||
prompt_name: str
|
||||
"""Name of the prompt used to generate this transcript."""
|
||||
text: str | None = None
|
||||
|
||||
@@ -28,12 +28,14 @@ class TranscriptionResult:
|
||||
prompt_name: str
|
||||
model: str
|
||||
|
||||
def to_transcript(self, job_id: UUID) -> Transcript:
|
||||
def to_transcript(self, job_id: UUID, *, revision: int = 0) -> Transcript:
|
||||
"""Convert a TranscriptionResult to a Transcript model instance."""
|
||||
return Transcript(
|
||||
job_id=job_id,
|
||||
revision=revision,
|
||||
provider=self.provider,
|
||||
prompt_name=self.prompt_name,
|
||||
model=self.model,
|
||||
text=self.text,
|
||||
)
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ class TranscriptionService(ServiceBase):
|
||||
job_id=job_id,
|
||||
text=result.text,
|
||||
provider=result.provider,
|
||||
model=result.model,
|
||||
prompt_name=result.prompt_name,
|
||||
session=session,
|
||||
)
|
||||
@@ -125,6 +126,7 @@ class TranscriptionService(ServiceBase):
|
||||
text: str | None,
|
||||
error_detail: str | None = None,
|
||||
provider: str | None = None,
|
||||
model: str | None = None,
|
||||
prompt_name: str = DEFAULT_PROMPT_FILE,
|
||||
session: AsyncSession | None = None,
|
||||
) -> Transcript:
|
||||
@@ -139,6 +141,7 @@ class TranscriptionService(ServiceBase):
|
||||
job_id=job_id,
|
||||
revision=next_revision,
|
||||
provider=provider or self.settings.provider.value,
|
||||
model=model or _resolve_transcript_model(provider=self.provider, settings=self.settings),
|
||||
prompt_name=prompt_name,
|
||||
text=text,
|
||||
error_detail=error_detail,
|
||||
@@ -181,6 +184,17 @@ def _transcript_job_query(job_id: UUID):
|
||||
) # fmt: skip
|
||||
|
||||
|
||||
def _resolve_transcript_model(*, provider: TranscriptionProvider, settings: Settings) -> str:
|
||||
provider_model = getattr(provider, "model", None)
|
||||
if isinstance(provider_model, str) and provider_model.strip():
|
||||
return provider_model
|
||||
|
||||
if settings.provider_model and settings.provider_model.strip():
|
||||
return settings.provider_model
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
async def transcribe_document_image(
|
||||
image_path: str | Path,
|
||||
*,
|
||||
|
||||
@@ -126,6 +126,7 @@ async def _finalize_transcribed(
|
||||
text=result.text,
|
||||
error_detail=None,
|
||||
provider=result.provider,
|
||||
model=result.model,
|
||||
prompt_name=result.prompt_name,
|
||||
session=local_session,
|
||||
)
|
||||
@@ -142,6 +143,7 @@ async def _finalize_transcribed(
|
||||
text=result.text,
|
||||
error_detail=None,
|
||||
provider=result.provider,
|
||||
model=result.model,
|
||||
prompt_name=result.prompt_name,
|
||||
session=session,
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ def render_transcript_revision_row(
|
||||
"""Render one collapsible row for a single transcript revision."""
|
||||
status_label = "Failed" if transcript.error_detail else "Transcribed"
|
||||
header = f"Revision {transcript.revision} | {status_label}"
|
||||
caption = f"{transcript.provider} | {transcript.prompt_name} | {_format_created_at(transcript.created_at)}"
|
||||
caption = f"{transcript.provider} | {transcript.model} | {_format_created_at(transcript.created_at)}"
|
||||
|
||||
expansion = ui.expansion(text=header, caption=caption, value=initially_expanded, group="group").classes(
|
||||
f"{classes} rounded-borders bg-blue-grey-10"
|
||||
@@ -27,7 +27,7 @@ def render_transcript_revision_row(
|
||||
|
||||
with expansion, ui.column().classes("w-full q-gutter-y-sm q-pa-sm"):
|
||||
_metadata_row(label="Provider", value=transcript.provider)
|
||||
_metadata_row(label="Prompt", value=transcript.prompt_name)
|
||||
_metadata_row(label="Model", value=transcript.model)
|
||||
_metadata_row(label="Created", value=_format_created_at(transcript.created_at))
|
||||
|
||||
if transcript.text:
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""Tests for API error response envelope handlers."""
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
|
||||
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
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlmodel import desc
|
||||
from sqlmodel import select
|
||||
|
||||
from transcription.config import Settings
|
||||
from transcription.models import Job, JobStatus, Transcript
|
||||
from transcription.models import Job
|
||||
from transcription.models import JobStatus
|
||||
from transcription.models import Transcript
|
||||
from transcription.providers.base import TranscriptionResult
|
||||
from transcription.services.upload import create_upload_job
|
||||
from transcription.services.store import create_upload_job
|
||||
from transcription.worker import process_next_queued_job
|
||||
|
||||
|
||||
@@ -16,24 +19,42 @@ from transcription.worker import process_next_queued_job
|
||||
class TestPipelineSuccessFlow:
|
||||
"""Verify end-to-end success lifecycle behavior."""
|
||||
|
||||
def test_upload_then_worker_persists_transcribed_terminal_state(self, session, tmp_path: Path, monkeypatch):
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_then_worker_persists_transcribed_terminal_state(
|
||||
self,
|
||||
async_session,
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
):
|
||||
"""Upload followed by worker processing persists transcript and transcribed status."""
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
upload_result = create_upload_job(
|
||||
upload_result = await create_upload_job(
|
||||
filename="pipeline.jpg",
|
||||
file_bytes=b"pipeline-bytes",
|
||||
session=session,
|
||||
session=async_session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
def _fake_transcribe(_path: str) -> TranscriptionResult:
|
||||
return TranscriptionResult(text="Pipeline transcript", provider="openrouter", model="test-model")
|
||||
async def _fake_transcribe(_path: str) -> TranscriptionResult:
|
||||
return TranscriptionResult(
|
||||
text="Pipeline transcript",
|
||||
provider="openrouter",
|
||||
prompt_name="transcribe_document.md",
|
||||
model="test-model",
|
||||
)
|
||||
|
||||
monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
|
||||
monkeypatch.setattr("transcription.services.workflows.transcribe_document_image", _fake_transcribe)
|
||||
|
||||
processed = process_next_queued_job(session=session)
|
||||
job = session.get(Job, upload_result.job_id)
|
||||
transcript = session.exec(select(Transcript).where(Transcript.job_id == upload_result.job_id)).first()
|
||||
processed = await process_next_queued_job(session=async_session)
|
||||
job = await async_session.get(Job, upload_result.job_id)
|
||||
transcript = (
|
||||
await async_session.exec(
|
||||
select(Transcript)
|
||||
.where(Transcript.job_id == upload_result.job_id)
|
||||
.order_by(desc(Transcript.revision))
|
||||
.limit(1)
|
||||
)
|
||||
).first()
|
||||
|
||||
assert processed is True
|
||||
assert job is not None
|
||||
@@ -47,24 +68,37 @@ class TestPipelineSuccessFlow:
|
||||
class TestPipelineFailureFlow:
|
||||
"""Verify end-to-end failure lifecycle behavior."""
|
||||
|
||||
def test_upload_then_worker_persists_failed_terminal_state(self, session, tmp_path: Path, monkeypatch):
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_then_worker_persists_failed_terminal_state(
|
||||
self,
|
||||
async_session,
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
):
|
||||
"""Upload followed by worker processing persists error detail and failed status."""
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
upload_result = create_upload_job(
|
||||
upload_result = await create_upload_job(
|
||||
filename="pipeline.jpg",
|
||||
file_bytes=b"pipeline-bytes",
|
||||
session=session,
|
||||
session=async_session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
def _fake_transcribe(_path: str) -> TranscriptionResult:
|
||||
async def _fake_transcribe(_path: str) -> TranscriptionResult:
|
||||
raise RuntimeError("pipeline provider failure")
|
||||
|
||||
monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
|
||||
monkeypatch.setattr("transcription.services.workflows.transcribe_document_image", _fake_transcribe)
|
||||
|
||||
processed = process_next_queued_job(session=session)
|
||||
job = session.get(Job, upload_result.job_id)
|
||||
transcript = session.exec(select(Transcript).where(Transcript.job_id == upload_result.job_id)).first()
|
||||
processed = await process_next_queued_job(session=async_session)
|
||||
job = await async_session.get(Job, upload_result.job_id)
|
||||
transcript = (
|
||||
await async_session.exec(
|
||||
select(Transcript)
|
||||
.where(Transcript.job_id == upload_result.job_id)
|
||||
.order_by(desc(Transcript.revision))
|
||||
.limit(1)
|
||||
)
|
||||
).first()
|
||||
|
||||
assert processed is True
|
||||
assert job is not None
|
||||
|
||||
@@ -5,8 +5,10 @@ from types import SimpleNamespace
|
||||
import pytest
|
||||
|
||||
from transcription.config import Settings
|
||||
from transcription.providers.base import ProviderError, ProviderResponseError
|
||||
from transcription.providers.openrouter import DEFAULT_OPENROUTER_MODEL, OpenRouterTranscriptionProvider
|
||||
from transcription.providers.base import ProviderError
|
||||
from transcription.providers.base import ProviderResponseError
|
||||
from transcription.providers.openrouter import DEFAULT_OPENROUTER_MODEL
|
||||
from transcription.providers.openrouter import OpenRouterTranscriptionProvider
|
||||
|
||||
|
||||
class _FakeChat:
|
||||
@@ -15,7 +17,7 @@ class _FakeChat:
|
||||
self._error = error
|
||||
self.calls = []
|
||||
|
||||
def send(self, **kwargs):
|
||||
async def send_async(self, **kwargs):
|
||||
self.calls.append(kwargs)
|
||||
if self._error:
|
||||
raise self._error
|
||||
@@ -48,7 +50,8 @@ class TestOpenRouterProviderInit:
|
||||
class TestOpenRouterProviderTranscribe:
|
||||
"""Verify OpenRouter request construction and response parsing."""
|
||||
|
||||
def test_includes_optional_referer_and_title_when_set(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_includes_optional_referer_and_title_when_set(self):
|
||||
"""Transcribe sends app attribution fields when configured."""
|
||||
response = {"model": "vendor/model-a", "choices": [{"message": {"content": "Transcript text"}}]}
|
||||
client = _FakeClient(response=response)
|
||||
@@ -59,7 +62,7 @@ class TestOpenRouterProviderTranscribe:
|
||||
)
|
||||
provider = OpenRouterTranscriptionProvider(settings=settings, client=client)
|
||||
|
||||
result = provider.transcribe(
|
||||
result = await provider.transcribe(
|
||||
prompt_text="Prompt body",
|
||||
image_bytes=b"img-bytes",
|
||||
mime_type="image/png",
|
||||
@@ -70,7 +73,8 @@ class TestOpenRouterProviderTranscribe:
|
||||
assert send_call["x_open_router_title"] == "Transcription App"
|
||||
assert result.text == "Transcript text"
|
||||
|
||||
def test_parses_successful_response_text(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_parses_successful_response_text(self):
|
||||
"""Transcribe returns normalized text from a valid response payload."""
|
||||
response = {
|
||||
"model": "vendor/model-b",
|
||||
@@ -81,7 +85,7 @@ class TestOpenRouterProviderTranscribe:
|
||||
client=_FakeClient(response=response),
|
||||
)
|
||||
|
||||
result = provider.transcribe(
|
||||
result = await provider.transcribe(
|
||||
prompt_text="Prompt body",
|
||||
image_bytes=b"img-bytes",
|
||||
mime_type="image/jpeg",
|
||||
@@ -91,7 +95,8 @@ class TestOpenRouterProviderTranscribe:
|
||||
assert result.provider == "openrouter"
|
||||
assert result.model == "vendor/model-b"
|
||||
|
||||
def test_maps_sdk_exception_to_provider_error(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_maps_sdk_exception_to_provider_error(self):
|
||||
"""Transcribe converts SDK failures to ProviderError."""
|
||||
provider = OpenRouterTranscriptionProvider(
|
||||
settings=Settings(openrouter_api_key="test-key"),
|
||||
@@ -99,13 +104,14 @@ class TestOpenRouterProviderTranscribe:
|
||||
)
|
||||
|
||||
with pytest.raises(ProviderError):
|
||||
provider.transcribe(
|
||||
await provider.transcribe(
|
||||
prompt_text="Prompt body",
|
||||
image_bytes=b"img-bytes",
|
||||
mime_type="image/png",
|
||||
)
|
||||
|
||||
def test_raises_on_empty_or_invalid_response(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_on_empty_or_invalid_response(self):
|
||||
"""Transcribe raises ProviderResponseError for missing completion text."""
|
||||
provider = OpenRouterTranscriptionProvider(
|
||||
settings=Settings(openrouter_api_key="test-key"),
|
||||
@@ -113,7 +119,7 @@ class TestOpenRouterProviderTranscribe:
|
||||
)
|
||||
|
||||
with pytest.raises(ProviderResponseError):
|
||||
provider.transcribe(
|
||||
await provider.transcribe(
|
||||
prompt_text="Prompt body",
|
||||
image_bytes=b"img-bytes",
|
||||
mime_type="image/png",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import pytest
|
||||
|
||||
from transcription.services.base import ServiceBase
|
||||
|
||||
|
||||
class FakeService(ServiceBase):
|
||||
"""A fake service class for testing"""
|
||||
|
||||
|
||||
class TestServiceBase:
|
||||
class TestInitialization:
|
||||
|
||||
@@ -7,8 +7,9 @@ import pytest
|
||||
|
||||
from transcription.services.transcription import transcribe_document_image
|
||||
|
||||
|
||||
HAS_OPENROUTER_KEY = bool(os.getenv("OPENROUTER_API_KEY"))
|
||||
HAS_OPENROUTER_KEY = bool(
|
||||
os.getenv("TRANSCRIPTION_OPENROUTER_API_KEY") or os.getenv("OPENROUTER_API_KEY")
|
||||
)
|
||||
|
||||
REAL_IMAGES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "real"
|
||||
ARTIFACTS_DIR = Path(__file__).resolve().parents[1] / "artifacts" / "transcriptions"
|
||||
@@ -18,7 +19,10 @@ pytestmark = [
|
||||
pytest.mark.external,
|
||||
pytest.mark.skipif(
|
||||
not HAS_OPENROUTER_KEY,
|
||||
reason="Set OPENROUTER_API_KEY to run external real-image tests.",
|
||||
reason=(
|
||||
"Set TRANSCRIPTION_OPENROUTER_API_KEY "
|
||||
"(or legacy OPENROUTER_API_KEY) to run external real-image tests."
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
+26
-10
@@ -25,14 +25,21 @@ class TestAppLifespan:
|
||||
"""Startup initializes logging, schema, directories, and worker resources."""
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr("transcription.app.setup_logging", lambda: calls.append("logging"))
|
||||
monkeypatch.setattr("transcription.app.create_all", lambda **_kwargs: calls.append("schema"))
|
||||
monkeypatch.setattr("transcription.app.configure_logging", lambda: calls.append("logging"))
|
||||
|
||||
async def _create_all(**_kwargs):
|
||||
calls.append("schema")
|
||||
|
||||
monkeypatch.setattr("transcription.app.create_all", _create_all)
|
||||
monkeypatch.setattr(
|
||||
"transcription.app.initialize_database_runtime",
|
||||
lambda **_kwargs: type("_Runtime", (), {"engine": object()})(),
|
||||
lambda **_kwargs: type("_Runtime", (), {"engine": object(), "session_factory": object()})(),
|
||||
)
|
||||
monkeypatch.setattr("transcription.app.dispose_database_runtime", lambda: calls.append("dispose_db"))
|
||||
monkeypatch.setattr("transcription.app.should_bootstrap_schema", lambda _settings: True)
|
||||
|
||||
async def _cleanup_database():
|
||||
calls.append("dispose_db")
|
||||
|
||||
monkeypatch.setattr("transcription.app.cleanup_database", _cleanup_database)
|
||||
monkeypatch.setattr("transcription.app._start_worker", lambda _app: calls.append("start_worker"))
|
||||
monkeypatch.setattr("transcription.app._stop_worker", lambda _app: calls.append("stop_worker"))
|
||||
|
||||
@@ -41,6 +48,7 @@ class TestAppLifespan:
|
||||
calls.append("mkdir")
|
||||
|
||||
class _Settings:
|
||||
should_bootstrap_schema = True
|
||||
upload_dir = _Dir()
|
||||
prompt_dir = _Dir()
|
||||
|
||||
@@ -60,14 +68,21 @@ class TestAppLifespan:
|
||||
"""Shutdown signals and stops worker resources cleanly."""
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr("transcription.app.setup_logging", lambda: None)
|
||||
monkeypatch.setattr("transcription.app.create_all", lambda **_kwargs: None)
|
||||
monkeypatch.setattr("transcription.app.configure_logging", lambda: None)
|
||||
|
||||
async def _create_all(**_kwargs):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("transcription.app.create_all", _create_all)
|
||||
monkeypatch.setattr(
|
||||
"transcription.app.initialize_database_runtime",
|
||||
lambda **_kwargs: type("_Runtime", (), {"engine": object()})(),
|
||||
lambda **_kwargs: type("_Runtime", (), {"engine": object(), "session_factory": object()})(),
|
||||
)
|
||||
monkeypatch.setattr("transcription.app.dispose_database_runtime", lambda: calls.append("dispose_db"))
|
||||
monkeypatch.setattr("transcription.app.should_bootstrap_schema", lambda _settings: True)
|
||||
|
||||
async def _cleanup_database():
|
||||
calls.append("dispose_db")
|
||||
|
||||
monkeypatch.setattr("transcription.app.cleanup_database", _cleanup_database)
|
||||
monkeypatch.setattr("transcription.app._start_worker", lambda _app: calls.append("start_worker"))
|
||||
monkeypatch.setattr("transcription.app._stop_worker", lambda _app: calls.append("stop_worker"))
|
||||
|
||||
@@ -76,6 +91,7 @@ class TestAppLifespan:
|
||||
return None
|
||||
|
||||
class _Settings:
|
||||
should_bootstrap_schema = True
|
||||
upload_dir = _Dir()
|
||||
prompt_dir = _Dir()
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ from pathlib import Path
|
||||
import pytest
|
||||
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:
|
||||
@@ -19,14 +20,14 @@ class TestSettingsLoading:
|
||||
"""Verify Settings construction and required-field validation."""
|
||||
|
||||
def test_loads_from_env(self, monkeypatch):
|
||||
"""Settings constructs when OPENROUTER_API_KEY is provided."""
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "test-key-xyz")
|
||||
"""Settings constructs when TRANSCRIPTION_OPENROUTER_API_KEY is provided."""
|
||||
monkeypatch.setenv("TRANSCRIPTION_OPENROUTER_API_KEY", "test-key-xyz")
|
||||
settings = Settings()
|
||||
assert settings.openrouter_api_key == "test-key-xyz"
|
||||
|
||||
def test_requires_api_key(self, monkeypatch):
|
||||
"""Settings raises ValidationError when OPENROUTER_API_KEY is missing."""
|
||||
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
||||
"""Settings raises ValidationError when required provider API key is missing."""
|
||||
monkeypatch.delenv("TRANSCRIPTION_OPENROUTER_API_KEY", raising=False)
|
||||
with pytest.raises(ValidationError):
|
||||
Settings(_env_file=None)
|
||||
|
||||
|
||||
+68
-23
@@ -1,14 +1,18 @@
|
||||
"""Tests for transcription.db — schema bootstrap and session factory."""
|
||||
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
import pytest
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlmodel import SQLModel
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlmodel.pool import StaticPool
|
||||
|
||||
|
||||
def _in_memory_engine():
|
||||
"""Create a fresh in-memory SQLite engine for isolated db tests."""
|
||||
return create_engine(
|
||||
"sqlite://",
|
||||
return create_async_engine(
|
||||
"sqlite+aiosqlite://",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
@@ -17,18 +21,21 @@ def _in_memory_engine():
|
||||
class TestSchemaBootstrap:
|
||||
"""Verify create_all produces the expected table set."""
|
||||
|
||||
def test_create_all_creates_expected_tables(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_all_creates_expected_tables(self):
|
||||
"""After create_all(), document, job, and transcript tables exist."""
|
||||
engine = _in_memory_engine()
|
||||
# Ensure models are imported so metadata is populated
|
||||
from transcription.models import Document, Job, Transcript # noqa: F401
|
||||
|
||||
import transcription.db as db_module
|
||||
from transcription.models import Document # noqa: F401
|
||||
from transcription.models import Job # noqa: F401
|
||||
from transcription.models import Transcript # noqa: F401
|
||||
|
||||
db_module.create_all(engine=engine)
|
||||
await db_module.create_all(engine=engine)
|
||||
|
||||
inspector = inspect(engine)
|
||||
table_names = set(inspector.get_table_names())
|
||||
async with engine.connect() as connection:
|
||||
table_names = set(await connection.run_sync(lambda conn: inspect(conn).get_table_names()))
|
||||
await engine.dispose()
|
||||
assert "document" in table_names
|
||||
assert "job" in table_names
|
||||
assert "transcript" in table_names
|
||||
@@ -37,31 +44,37 @@ class TestSchemaBootstrap:
|
||||
class TestSessionFactory:
|
||||
"""Verify get_session yields and cleans up sessions."""
|
||||
|
||||
def test_get_session_yields_session(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_session_yields_session(self):
|
||||
"""get_session() yields a usable Session object."""
|
||||
engine = _in_memory_engine()
|
||||
SQLModel.metadata.create_all(engine)
|
||||
async with engine.begin() as connection:
|
||||
await connection.run_sync(SQLModel.metadata.create_all)
|
||||
|
||||
import transcription.db as db_module
|
||||
|
||||
with db_module.get_session(engine=engine) as session:
|
||||
assert isinstance(session, Session)
|
||||
factory = db_module.async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
async with db_module.get_session(session_factory=factory) as session:
|
||||
assert isinstance(session, AsyncSession)
|
||||
await engine.dispose()
|
||||
|
||||
def test_session_is_closed_after_generator_exit(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_is_closed_after_generator_exit(self):
|
||||
"""After the context manager exits, the session is closed."""
|
||||
engine = _in_memory_engine()
|
||||
SQLModel.metadata.create_all(engine)
|
||||
async with engine.begin() as connection:
|
||||
await connection.run_sync(SQLModel.metadata.create_all)
|
||||
|
||||
import transcription.db as db_module
|
||||
|
||||
with db_module.get_session(engine=engine) as session:
|
||||
factory = db_module.async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
async with db_module.get_session(session_factory=factory) as session:
|
||||
# Session is usable inside the context
|
||||
session.execute(text("SELECT 1"))
|
||||
await session.exec(text("SELECT 1"))
|
||||
captured = session
|
||||
|
||||
# After exiting, the session's internal connection is released
|
||||
# (no active transaction bound to the session)
|
||||
assert captured._transaction is None
|
||||
assert captured.sync_session._transaction is None
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
class TestBootstrapPolicy:
|
||||
@@ -72,7 +85,11 @@ class TestBootstrapPolicy:
|
||||
from transcription.config import Settings
|
||||
from transcription.db import should_bootstrap_schema
|
||||
|
||||
settings = Settings(openrouter_api_key="test-key", environment="production")
|
||||
settings = Settings(
|
||||
openrouter_api_key="test-key",
|
||||
environment="production",
|
||||
bootstrap_schema_on_startup=None,
|
||||
)
|
||||
assert should_bootstrap_schema(settings) is False
|
||||
|
||||
def test_development_defaults_to_bootstrap(self):
|
||||
@@ -80,7 +97,11 @@ class TestBootstrapPolicy:
|
||||
from transcription.config import Settings
|
||||
from transcription.db import should_bootstrap_schema
|
||||
|
||||
settings = Settings(openrouter_api_key="test-key", environment="development")
|
||||
settings = Settings(
|
||||
openrouter_api_key="test-key",
|
||||
environment="development",
|
||||
bootstrap_schema_on_startup=None,
|
||||
)
|
||||
assert should_bootstrap_schema(settings) is True
|
||||
|
||||
def test_explicit_override_wins(self):
|
||||
@@ -94,3 +115,27 @@ class TestBootstrapPolicy:
|
||||
bootstrap_schema_on_startup=True,
|
||||
)
|
||||
assert should_bootstrap_schema(settings) is True
|
||||
|
||||
|
||||
class TestRuntimeSqliteEngineConfig:
|
||||
"""Verify sqlite runtime engine configuration from settings."""
|
||||
|
||||
def test_in_memory_sqlite_uses_static_pool_by_default(self):
|
||||
"""In-memory sqlite URLs get StaticPool for consistent connections."""
|
||||
from transcription.config import Settings
|
||||
from transcription.db.runtime import _build_engine
|
||||
|
||||
settings = Settings(openrouter_api_key="test-key", database_url="sqlite://")
|
||||
engine = _build_engine(settings)
|
||||
|
||||
assert engine.sync_engine.pool.__class__.__name__ == "StaticPool"
|
||||
|
||||
def test_file_sqlite_does_not_force_static_pool(self):
|
||||
"""File-backed sqlite URLs keep default pool behavior."""
|
||||
from transcription.config import Settings
|
||||
from transcription.db.runtime import _build_engine
|
||||
|
||||
settings = Settings(openrouter_api_key="test-key", database_url="sqlite:///./runtime-test.db")
|
||||
engine = _build_engine(settings)
|
||||
|
||||
assert engine.sync_engine.pool.__class__.__name__ != "StaticPool"
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
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
|
||||
|
||||
+77
-16
@@ -5,7 +5,10 @@ from uuid import UUID
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from transcription.models import Document, Job, JobStatus, Transcript
|
||||
from transcription.models import Document
|
||||
from transcription.models import Job
|
||||
from transcription.models import JobStatus
|
||||
from transcription.models import Transcript
|
||||
|
||||
|
||||
def _make_document(**overrides) -> Document:
|
||||
@@ -113,7 +116,14 @@ class TestTranscriptModel:
|
||||
"""A Transcript with text set and error_detail None persists correctly."""
|
||||
doc = _persist_document(session)
|
||||
job = _persist_job(session, doc)
|
||||
transcript = Transcript(job_id=job.id, text="Dear Sir, ...")
|
||||
transcript = Transcript(
|
||||
job_id=job.id,
|
||||
revision=0,
|
||||
provider="openrouter",
|
||||
model="google/gemini-2.5-flash",
|
||||
prompt_name="transcribe_document.md",
|
||||
text="Dear Sir, ...",
|
||||
)
|
||||
session.add(transcript)
|
||||
session.commit()
|
||||
session.refresh(transcript)
|
||||
@@ -127,7 +137,14 @@ class TestTranscriptModel:
|
||||
"""A Transcript with text None and error_detail set persists correctly."""
|
||||
doc = _persist_document(session)
|
||||
job = _persist_job(session, doc)
|
||||
transcript = Transcript(job_id=job.id, error_detail="Provider timeout")
|
||||
transcript = Transcript(
|
||||
job_id=job.id,
|
||||
revision=0,
|
||||
provider="openrouter",
|
||||
model="google/gemini-2.5-flash",
|
||||
prompt_name="transcribe_document.md",
|
||||
error_detail="Provider timeout",
|
||||
)
|
||||
session.add(transcript)
|
||||
session.commit()
|
||||
session.refresh(transcript)
|
||||
@@ -137,19 +154,46 @@ class TestTranscriptModel:
|
||||
assert fetched.text is None
|
||||
assert fetched.error_detail == "Provider timeout"
|
||||
|
||||
def test_job_id_is_unique(self, session):
|
||||
"""Inserting two transcripts with the same job_id raises an integrity error."""
|
||||
def test_job_revision_pair_is_unique(self, session):
|
||||
"""Duplicate job_id and revision combinations raise an integrity error."""
|
||||
doc = _persist_document(session)
|
||||
job = _persist_job(session, doc)
|
||||
|
||||
t1 = Transcript(job_id=job.id, text="First")
|
||||
session.add(t1)
|
||||
session.add(
|
||||
Transcript(
|
||||
job_id=job.id,
|
||||
revision=0,
|
||||
provider="openrouter",
|
||||
model="google/gemini-2.5-flash",
|
||||
prompt_name="transcribe_document.md",
|
||||
text="First",
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
Transcript(
|
||||
job_id=job.id,
|
||||
revision=1,
|
||||
provider="openrouter",
|
||||
model="google/gemini-2.5-flash",
|
||||
prompt_name="transcribe_document.md",
|
||||
text="Second",
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
t2 = Transcript(job_id=job.id, text="Duplicate")
|
||||
session.add(t2)
|
||||
session.add(
|
||||
Transcript(
|
||||
job_id=job.id,
|
||||
revision=1,
|
||||
provider="openrouter",
|
||||
model="google/gemini-2.5-flash",
|
||||
prompt_name="transcribe_document.md",
|
||||
text="Duplicate",
|
||||
)
|
||||
)
|
||||
with pytest.raises(IntegrityError):
|
||||
session.commit()
|
||||
session.rollback()
|
||||
|
||||
|
||||
class TestRelationships:
|
||||
@@ -165,15 +209,32 @@ class TestRelationships:
|
||||
assert len(doc.jobs) == 2
|
||||
assert all(isinstance(j, Job) for j in doc.jobs)
|
||||
|
||||
def test_job_exposes_transcript(self, session):
|
||||
"""job.transcript returns the linked Transcript."""
|
||||
def test_job_exposes_transcripts(self, session):
|
||||
"""job.transcripts returns linked Transcript history."""
|
||||
doc = _persist_document(session)
|
||||
job = _persist_job(session, doc)
|
||||
transcript = Transcript(job_id=job.id, text="Transcribed text")
|
||||
session.add(transcript)
|
||||
session.add(
|
||||
Transcript(
|
||||
job_id=job.id,
|
||||
revision=0,
|
||||
provider="openrouter",
|
||||
model="google/gemini-2.5-flash",
|
||||
prompt_name="transcribe_document.md",
|
||||
text="Transcribed text",
|
||||
)
|
||||
)
|
||||
session.add(
|
||||
Transcript(
|
||||
job_id=job.id,
|
||||
revision=1,
|
||||
provider="openrouter",
|
||||
model="google/gemini-2.5-flash",
|
||||
prompt_name="transcribe_document.md",
|
||||
text="Transcribed text v2",
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
session.refresh(job)
|
||||
assert job.transcript is not None
|
||||
assert isinstance(job.transcript, Transcript)
|
||||
assert job.transcript.text == "Transcribed text"
|
||||
assert len(job.transcripts) == 2
|
||||
assert all(isinstance(t, Transcript) for t in job.transcripts)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROMPT_PATH = Path("prompts/transcribe_document.md")
|
||||
|
||||
|
||||
|
||||
+45
-6
@@ -10,38 +10,63 @@ from uuid import UUID
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import delete
|
||||
|
||||
from transcription.app import create_app
|
||||
from transcription.config import Settings
|
||||
from transcription.config import _settings
|
||||
from transcription.db import create_all
|
||||
from transcription.db import get_session
|
||||
from transcription.db import initialize_database_runtime
|
||||
from transcription.models import Document
|
||||
from transcription.models import Job
|
||||
from transcription.models import JobStatus
|
||||
from transcription.models import Transcript
|
||||
|
||||
TranscriptSeed = tuple[int, str | None, str | None]
|
||||
|
||||
@pytest.fixture
|
||||
def app_client(tmp_path: Path) -> tuple[FastAPI, TestClient]:
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_client(tmp_path_factory: pytest.TempPathFactory) -> tuple[FastAPI, TestClient]:
|
||||
"""Provide a real application and test client backed by in-memory SQLite."""
|
||||
tmp_path = tmp_path_factory.mktemp("ui")
|
||||
settings = Settings(
|
||||
openrouter_api_key="test-key",
|
||||
database_url="sqlite:///:memory:",
|
||||
environment="test",
|
||||
bootstrap_schema_on_startup=True,
|
||||
upload_dir=tmp_path / "uploads",
|
||||
prompt_dir=tmp_path / "prompts",
|
||||
)
|
||||
_settings.set(settings)
|
||||
|
||||
app = create_app()
|
||||
app.state.runtime = initialize_database_runtime(settings=settings)
|
||||
asyncio.run(create_all(engine=app.state.runtime.engine))
|
||||
with TestClient(app) as client:
|
||||
yield app, client
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_ui_database(app_client: tuple[FastAPI, TestClient]) -> None:
|
||||
"""Reset UI-facing tables before each test for isolation."""
|
||||
app, _ = app_client
|
||||
|
||||
async def _clear() -> None:
|
||||
async with get_session(session_factory=app.state.runtime.session_factory) as session:
|
||||
await session.exec(delete(Transcript))
|
||||
await session.exec(delete(Job))
|
||||
await session.exec(delete(Document))
|
||||
await session.commit()
|
||||
|
||||
asyncio.run(_clear())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seed_job(app_client: tuple[FastAPI, TestClient]) -> Callable[..., UUID]:
|
||||
"""Return a helper for inserting a document/job/transcript trio."""
|
||||
app, _ = app_client
|
||||
fixtures_dir = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "valid"
|
||||
|
||||
def _seed(
|
||||
*,
|
||||
@@ -49,10 +74,17 @@ def seed_job(app_client: tuple[FastAPI, TestClient]) -> Callable[..., UUID]:
|
||||
status: JobStatus = JobStatus.TRANSCRIBED,
|
||||
transcript_text: str | None = "Sample transcript text",
|
||||
error_detail: str | None = None,
|
||||
transcript_revisions: list[TranscriptSeed] | None = None,
|
||||
source_file: Path | None = None,
|
||||
) -> UUID:
|
||||
async def _insert() -> UUID:
|
||||
async with get_session(session_factory=app.state.runtime.session_factory) as session:
|
||||
document = Document(filename=filename, file_path=f"uploads/{filename}")
|
||||
stored_path = app.state.settings.upload_dir / filename
|
||||
stored_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
source_path = source_file or fixtures_dir / "small_png.png"
|
||||
stored_path.write_bytes(source_path.read_bytes())
|
||||
|
||||
document = Document(filename=filename, file_path=str(stored_path))
|
||||
session.add(document)
|
||||
await session.flush()
|
||||
|
||||
@@ -60,14 +92,21 @@ def seed_job(app_client: tuple[FastAPI, TestClient]) -> Callable[..., UUID]:
|
||||
session.add(job)
|
||||
await session.flush()
|
||||
|
||||
if transcript_text is not None or error_detail is not None:
|
||||
revisions = transcript_revisions
|
||||
if revisions is None and (transcript_text is not None or error_detail is not None):
|
||||
revisions = [(0, transcript_text, error_detail)]
|
||||
|
||||
if revisions is not None:
|
||||
for revision, revision_text, revision_error in revisions:
|
||||
session.add(
|
||||
Transcript(
|
||||
job_id=job.id,
|
||||
revision=revision,
|
||||
provider="openrouter",
|
||||
model="google/gemini-2.5-flash",
|
||||
prompt_name="transcribe_document",
|
||||
text=transcript_text,
|
||||
error_detail=error_detail,
|
||||
text=revision_text,
|
||||
error_detail=revision_error,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+63
-39
@@ -1,53 +1,77 @@
|
||||
"""Tests for the jobs page route."""
|
||||
|
||||
from uuid import UUID
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from transcription.ui import register_pages
|
||||
from transcription.ui.pages import jobs_page
|
||||
from transcription.ui.pages.jobs_page import JobTableRow
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(monkeypatch):
|
||||
"""Provide a minimal app client with jobs data patched for rendering."""
|
||||
|
||||
async def _fetch_jobs_stub():
|
||||
return [
|
||||
JobTableRow(
|
||||
id=UUID("00000000-0000-0000-0000-000000000001"),
|
||||
status="queued",
|
||||
filename="sample.pdf",
|
||||
retry_count=2,
|
||||
created_at="2026-01-01T12:00:00+00:00",
|
||||
updated_at="2026-01-01T12:01:00+00:00",
|
||||
)
|
||||
]
|
||||
|
||||
monkeypatch.setattr(jobs_page, "fetch_jobs", _fetch_jobs_stub)
|
||||
|
||||
app = FastAPI()
|
||||
register_pages(app)
|
||||
with TestClient(app) as test_client:
|
||||
yield test_client
|
||||
from transcription.models import JobStatus
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestPageRendering:
|
||||
"""Verify the jobs page is available and includes the main controls."""
|
||||
"""Verify jobs routes render correctly with real app wiring."""
|
||||
|
||||
def test_jobs_page_renders_expected_controls(self, client, monkeypatch):
|
||||
"""GET /ui/jobs returns the page shell and jobs controls."""
|
||||
response = client.get("/ui/jobs")
|
||||
|
||||
async def _fetch_jobs_empty():
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(jobs_page, "fetch_jobs", _fetch_jobs_empty)
|
||||
def test_jobs_page_renders_empty_state(self, app_client):
|
||||
"""GET /ui/jobs renders the page and empty-state text when no jobs exist."""
|
||||
_, client = app_client
|
||||
response = client.get("/ui/jobs")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Transcription Jobs" in response.text
|
||||
assert "No jobs yet." in response.text
|
||||
|
||||
def test_jobs_page_lists_seeded_jobs(self, app_client, seed_job):
|
||||
"""GET /ui/jobs lists seeded jobs from the in-memory database."""
|
||||
_, client = app_client
|
||||
seed_job(filename="sample.pdf", status=JobStatus.TRANSCRIBED, transcript_text="done")
|
||||
|
||||
response = client.get("/ui/jobs")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "sample.pdf" in response.text
|
||||
assert "transcribed" in response.text
|
||||
|
||||
def test_job_detail_page_renders_seeded_job(self, app_client, seed_job):
|
||||
"""GET /ui/jobs/{job_id} renders detail content for a real seeded job."""
|
||||
_, client = app_client
|
||||
fixture_path = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "valid" / "single_page_pdf.pdf"
|
||||
job_id = seed_job(
|
||||
filename="detail.pdf",
|
||||
status=JobStatus.TRANSCRIBED,
|
||||
transcript_revisions=[
|
||||
(0, None, "first attempt failed"),
|
||||
(1, "hello", None),
|
||||
],
|
||||
source_file=fixture_path,
|
||||
)
|
||||
|
||||
response = client.get(f"/ui/jobs/{job_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Job Detail" in response.text
|
||||
assert "Job overview" in response.text
|
||||
assert "detail.pdf" in response.text
|
||||
assert "Transcripts" in response.text
|
||||
assert "Revision" in response.text
|
||||
assert "first attempt failed" in response.text
|
||||
assert "hello" in response.text
|
||||
assert "Document preview" in response.text
|
||||
assert "/uploads/detail.pdf" in response.text
|
||||
|
||||
def test_job_detail_page_rejects_invalid_id(self, app_client):
|
||||
"""GET /ui/jobs/{job_id} shows validation feedback for malformed IDs."""
|
||||
_, client = app_client
|
||||
response = client.get("/ui/jobs/not-a-uuid")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Invalid job id" in response.text
|
||||
|
||||
def test_job_detail_page_handles_missing_job(self, app_client):
|
||||
"""GET /ui/jobs/{job_id} shows not-found state for unknown IDs."""
|
||||
_, client = app_client
|
||||
missing_id = uuid4()
|
||||
response = client.get(f"/ui/jobs/{missing_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Job not found" in response.text
|
||||
|
||||
@@ -1,39 +1,18 @@
|
||||
"""Tests for UI page registration wiring."""
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
|
||||
from transcription.ui import register_pages
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestPageRegistration:
|
||||
"""Verify page registration and route wiring."""
|
||||
"""Verify page registration and mounted UI routes."""
|
||||
|
||||
def test_register_pages_wires_upload_jobs_and_mount(self, monkeypatch):
|
||||
"""register_pages registers pages and mounts NiceGUI at /ui."""
|
||||
calls: list[str] = []
|
||||
def test_ui_mount_serves_registered_pages(self, app_client):
|
||||
"""Mounted UI routes respond successfully when the full app is created."""
|
||||
_, client = app_client
|
||||
|
||||
def _record_upload() -> None:
|
||||
calls.append("upload")
|
||||
upload_response = client.get("/ui/upload")
|
||||
jobs_response = client.get("/ui/jobs")
|
||||
|
||||
def _record_jobs() -> None:
|
||||
calls.append("jobs")
|
||||
|
||||
def _record_run_with(
|
||||
_app: FastAPI,
|
||||
*,
|
||||
mount_path: str,
|
||||
show_welcome_message: bool,
|
||||
dark: bool,
|
||||
) -> None:
|
||||
calls.append(f"run_with:{mount_path}:{show_welcome_message}:{dark}")
|
||||
|
||||
monkeypatch.setattr("transcription.ui.register_upload_page", _record_upload)
|
||||
monkeypatch.setattr("transcription.ui.register_jobs_page", _record_jobs)
|
||||
monkeypatch.setattr("transcription.ui.ui.run_with", _record_run_with)
|
||||
|
||||
app = FastAPI()
|
||||
register_pages(app)
|
||||
|
||||
assert calls == ["upload", "jobs", "run_with:/ui:False:True"]
|
||||
assert upload_response.status_code == 200
|
||||
assert jobs_response.status_code == 200
|
||||
|
||||
@@ -1,55 +1,35 @@
|
||||
"""Tests for the upload page route."""
|
||||
|
||||
from pathlib import Path
|
||||
"""Tests for upload and entry-point routes."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from transcription.app import create_app
|
||||
from transcription.config import Settings
|
||||
from transcription.config import _settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path: Path):
|
||||
"""Provide a real app client backed by in-memory SQLite."""
|
||||
settings = Settings(
|
||||
openrouter_api_key="test-key",
|
||||
database_url="sqlite:///:memory:",
|
||||
environment="test",
|
||||
upload_dir=tmp_path / "uploads",
|
||||
prompt_dir=tmp_path / "prompts",
|
||||
)
|
||||
_settings.set(settings)
|
||||
|
||||
app = create_app()
|
||||
with TestClient(app) as test_client:
|
||||
yield test_client
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestPageRendering:
|
||||
"""Verify the upload page is available and includes the main controls."""
|
||||
"""Verify upload-related routes return working pages."""
|
||||
|
||||
def test_root_redirects_to_ui(self, client):
|
||||
def test_root_redirects_to_ui(self, app_client):
|
||||
"""GET / redirects to the UI mount point."""
|
||||
_, client = app_client
|
||||
response = client.get("/", follow_redirects=False)
|
||||
|
||||
assert response.status_code == 307
|
||||
assert response.headers["location"] == "/ui"
|
||||
|
||||
def test_ui_redirects_to_upload(self, client):
|
||||
def test_ui_redirects_to_upload(self, app_client):
|
||||
"""GET /ui redirects to the upload page."""
|
||||
_, client = app_client
|
||||
response = client.get("/ui", follow_redirects=False)
|
||||
|
||||
assert response.status_code == 307
|
||||
assert response.headers["location"] == "/ui/upload"
|
||||
|
||||
def test_upload_page_renders_expected_controls(self, client):
|
||||
def test_upload_page_renders_expected_controls(self, app_client):
|
||||
"""GET /ui/upload returns the page shell and upload controls."""
|
||||
_, client = app_client
|
||||
response = client.get("/ui/upload")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Upload Document" in response.text
|
||||
assert "Select document file" in response.text
|
||||
assert "Upload" in response.text
|
||||
assert "Jobs" in response.text
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from nicegui import ui
|
||||
|
||||
ui.colors(primary="#2563eb")
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Header
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
with ui.header(elevated=True).classes("items-center justify-between"):
|
||||
ui.label("Document Review").classes("text-xl font-bold")
|
||||
|
||||
with ui.row().classes("items-center"):
|
||||
ui.button(icon="zoom_out")
|
||||
ui.button(icon="zoom_in")
|
||||
ui.separator().props("vertical")
|
||||
ui.input("Search").props("dense outlined")
|
||||
ui.button("Export")
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Main split view
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
with ui.splitter(value=70).classes("w-full h-[calc(100vh-64px)]") as splitter:
|
||||
# ==============================================================
|
||||
# Left panel
|
||||
# ==============================================================
|
||||
|
||||
with splitter.before, ui.column().classes("w-full h-full items-center bg-slate-100"):
|
||||
# This can later become:
|
||||
# - interactive_image
|
||||
# - OpenSeadragon
|
||||
# - Leaflet
|
||||
# - custom HTML component
|
||||
|
||||
ui.image("https://picsum.photos/900/1200").classes("max-h-full max-w-full object-contain")
|
||||
|
||||
# ==============================================================
|
||||
# Right panel
|
||||
# ==============================================================
|
||||
|
||||
with splitter.after, ui.scroll_area().classes("w-full h-full"), ui.column().classes("w-full p-4 gap-3"):
|
||||
with ui.expansion("Summary", icon="description", value=True):
|
||||
ui.label("Short description of the document.")
|
||||
|
||||
with ui.expansion("OCR Text"):
|
||||
ui.textarea().props("readonly").classes("w-full")
|
||||
|
||||
with ui.expansion("Entities", value=True):
|
||||
ui.chip("Person")
|
||||
ui.chip("Organization")
|
||||
ui.chip("Date")
|
||||
|
||||
with ui.expansion("AI Analysis"):
|
||||
ui.markdown("""
|
||||
- Confidence: 98%
|
||||
- Language: English
|
||||
- Contains signature
|
||||
""")
|
||||
|
||||
with ui.expansion("Notes"):
|
||||
ui.textarea(placeholder="Add notes...").classes("w-full")
|
||||
|
||||
|
||||
ui.run(port=8081)
|
||||
Reference in New Issue
Block a user