generated from john/python-template
This commit is contained in:
@@ -4,10 +4,14 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Request
|
||||
from nicegui import ui
|
||||
from PIL import Image
|
||||
from PIL import UnidentifiedImageError
|
||||
|
||||
from transcription.config import Settings
|
||||
from transcription.db.models import ExecutionAttempt
|
||||
@@ -18,6 +22,7 @@ from transcription.services.errors import SourceDeleteBlockedError
|
||||
from transcription.services.errors import TranscriptionNotFoundError
|
||||
from transcription.services.evidence import EvidenceService
|
||||
from transcription.services.evidence import LatestExecutionAttempt
|
||||
from transcription.services.source_media import lookup_source_mime_type
|
||||
from transcription.services.sources import SourceService
|
||||
from transcription.ui.components.app_shell import render_navigation_header
|
||||
from transcription.ui.components.cards import archival_card
|
||||
@@ -43,6 +48,10 @@ from transcription.ui.theme import page_header
|
||||
|
||||
from ...db.session import SessionFactoryDep
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_WIDE_NARROW_ASPECT_RATIO_THRESHOLD = 2.2
|
||||
|
||||
|
||||
def register_page() -> None: # noqa: PLR0915
|
||||
"""Register sources list, detail, and deletion routes."""
|
||||
@@ -120,6 +129,7 @@ def register_page() -> None: # noqa: PLR0915
|
||||
sources_service = SourceService(session_factory=session_factory)
|
||||
evidence_service = EvidenceService(session_factory=session_factory)
|
||||
render_navigation_header(current_path="/sources")
|
||||
settings = resolve_runtime_settings(request)
|
||||
|
||||
parsed_source_id = parsed_record_id(source_id, noun="Source")
|
||||
if parsed_source_id is None:
|
||||
@@ -143,6 +153,7 @@ def register_page() -> None: # noqa: PLR0915
|
||||
return
|
||||
|
||||
original_transcription = _resolve_original_transcription(source=source, latest_attempt=latest_attempt)
|
||||
layout_mode = _resolve_source_detail_layout(source=source, settings=settings)
|
||||
|
||||
with ui.column().classes("w-full max-w-[1800px] mx-auto p-4 gap-4"):
|
||||
with section_header_row():
|
||||
@@ -176,23 +187,27 @@ def register_page() -> None: # noqa: PLR0915
|
||||
extra_classes="text-xs",
|
||||
)
|
||||
with ui.grid().classes("w-full grid-cols-12 gap-4"):
|
||||
with ui.column().classes("col-span-12 lg:col-span-8 gap-4"):
|
||||
with ui.grid().classes("w-full grid-cols-1 lg:grid-cols-2 gap-4"):
|
||||
with ui.column().classes("gap-2"):
|
||||
_render_source_navigation(navigation.previous_id, navigation.next_id)
|
||||
_render_source_viewer_zone(
|
||||
source,
|
||||
settings=resolve_runtime_settings(request),
|
||||
request=request,
|
||||
)
|
||||
_render_source_transcription_column(
|
||||
source=source,
|
||||
original_transcription=original_transcription,
|
||||
latest_job_source=latest_job_source,
|
||||
sources_service=sources_service,
|
||||
)
|
||||
_render_machine_candidates(
|
||||
if layout_mode == "wide_narrow":
|
||||
_render_wide_narrow_source_content(
|
||||
source=source,
|
||||
navigation=navigation,
|
||||
request=request,
|
||||
settings=settings,
|
||||
original_transcription=original_transcription,
|
||||
latest_job_source=latest_job_source,
|
||||
sources_service=sources_service,
|
||||
attempts=attempts,
|
||||
evidence_service=evidence_service,
|
||||
)
|
||||
else:
|
||||
_render_standard_source_content(
|
||||
source=source,
|
||||
navigation=navigation,
|
||||
request=request,
|
||||
settings=settings,
|
||||
original_transcription=original_transcription,
|
||||
latest_job_source=latest_job_source,
|
||||
sources_service=sources_service,
|
||||
attempts=attempts,
|
||||
evidence_service=evidence_service,
|
||||
)
|
||||
@@ -200,7 +215,10 @@ def register_page() -> None: # noqa: PLR0915
|
||||
source=source,
|
||||
latest_job_source=latest_job_source,
|
||||
latest_attempt=latest_attempt,
|
||||
settings=resolve_runtime_settings(request),
|
||||
settings=settings,
|
||||
column_classes="col-span-12 lg:col-span-4 gap-4"
|
||||
if layout_mode == "wide_narrow"
|
||||
else "col-span-12 lg:col-span-3 gap-4",
|
||||
)
|
||||
|
||||
@ui.page("/sources/{source_id}/delete")
|
||||
@@ -312,14 +330,72 @@ def _render_source_transcription_column(
|
||||
)
|
||||
|
||||
|
||||
def _render_standard_source_content(
|
||||
*,
|
||||
source: Source,
|
||||
navigation: SourceNavigation,
|
||||
request: Request,
|
||||
settings: Settings,
|
||||
original_transcription: str | None,
|
||||
latest_job_source: JobSource | None,
|
||||
sources_service: SourceService,
|
||||
attempts: list[ExecutionAttempt],
|
||||
evidence_service: EvidenceService,
|
||||
) -> None:
|
||||
with ui.column().classes("col-span-12 lg:col-span-4 gap-2"):
|
||||
_render_source_navigation(navigation.previous_id, navigation.next_id)
|
||||
_render_source_viewer_zone(source, settings=settings, request=request)
|
||||
with ui.column().classes("col-span-12 lg:col-span-5 gap-4"):
|
||||
_render_source_transcription_zone(
|
||||
source=source,
|
||||
original_transcription=original_transcription,
|
||||
latest_job_source=latest_job_source,
|
||||
sources_service=sources_service,
|
||||
)
|
||||
_render_machine_candidates(
|
||||
source=source,
|
||||
attempts=attempts,
|
||||
evidence_service=evidence_service,
|
||||
)
|
||||
|
||||
|
||||
def _render_wide_narrow_source_content(
|
||||
*,
|
||||
source: Source,
|
||||
navigation: SourceNavigation,
|
||||
request: Request,
|
||||
settings: Settings,
|
||||
original_transcription: str | None,
|
||||
latest_job_source: JobSource | None,
|
||||
sources_service: SourceService,
|
||||
attempts: list[ExecutionAttempt],
|
||||
evidence_service: EvidenceService,
|
||||
) -> None:
|
||||
with ui.column().classes("col-span-12 lg:col-span-8 gap-4"):
|
||||
_render_source_navigation(navigation.previous_id, navigation.next_id)
|
||||
_render_source_viewer_zone(source, settings=settings, request=request)
|
||||
_render_source_transcription_zone(
|
||||
source=source,
|
||||
original_transcription=original_transcription,
|
||||
latest_job_source=latest_job_source,
|
||||
sources_service=sources_service,
|
||||
)
|
||||
_render_machine_candidates(
|
||||
source=source,
|
||||
attempts=attempts,
|
||||
evidence_service=evidence_service,
|
||||
)
|
||||
|
||||
|
||||
def _render_source_metadata_column(
|
||||
*,
|
||||
source: Source,
|
||||
latest_job_source: JobSource | None,
|
||||
latest_attempt: LatestExecutionAttempt | None,
|
||||
settings: Settings,
|
||||
column_classes: str = "col-span-12 lg:col-span-4 gap-4",
|
||||
) -> None:
|
||||
with ui.column().classes("col-span-12 lg:col-span-4 gap-4"):
|
||||
with ui.column().classes(column_classes):
|
||||
_render_source_metadata_zone(source, settings=settings)
|
||||
_render_source_job_metadata_zone(
|
||||
latest_job_source,
|
||||
@@ -328,6 +404,35 @@ def _render_source_metadata_column(
|
||||
_render_source_revision_logistics_zone(source)
|
||||
|
||||
|
||||
def _resolve_source_detail_layout(*, source: Source, settings: Settings) -> str:
|
||||
media_type = lookup_source_mime_type(source.file_path)
|
||||
if not media_type.startswith("image/"):
|
||||
return "standard"
|
||||
absolute = (settings.upload_dir / Path(source.file_path)).resolve()
|
||||
dimensions = _read_image_dimensions(absolute)
|
||||
if dimensions is None:
|
||||
return "standard"
|
||||
width, height = dimensions
|
||||
if _is_wide_narrow_image(width=width, height=height):
|
||||
return "wide_narrow"
|
||||
return "standard"
|
||||
|
||||
|
||||
def _is_wide_narrow_image(*, width: int, height: int) -> bool:
|
||||
if width <= 0 or height <= 0:
|
||||
return False
|
||||
return (width / height) >= _WIDE_NARROW_ASPECT_RATIO_THRESHOLD
|
||||
|
||||
|
||||
def _read_image_dimensions(path: Path) -> tuple[int, int] | None:
|
||||
try:
|
||||
with Image.open(path) as image:
|
||||
return image.size
|
||||
except (FileNotFoundError, OSError, UnidentifiedImageError):
|
||||
logger.debug("Unable to read image dimensions for source detail layout: %s", path)
|
||||
return None
|
||||
|
||||
|
||||
def _render_source_metadata_zone(source: Source, *, settings: Settings) -> None:
|
||||
with archival_card(title="Source Metadata"):
|
||||
metadata_row("Upload Name:", source.upload_name)
|
||||
|
||||
Reference in New Issue
Block a user