Spaces:
Running
Running
| """DTOs for the Add Book wizard — uploads + probe. | |
| The wizard's three-step flow (per ``BACKEND_BUILD.md §13 Step 6`` + | |
| ``FRONTEND_BUILD.md`` Add Book page): | |
| 1. ``POST /uploads`` (multipart) → :class:`UploadResponse` — bytes land in | |
| a temp dir, the FE gets back a stable ``uploadId`` it can hand to the | |
| probe + create endpoints. | |
| 2. ``POST /books/probe`` body :class:`ProbeRequest` → :class:`ProbeResponse` | |
| — wraps ``src/lib/metadata_probe.py`` and returns enough metadata for | |
| the wizard to prefill the create form (title, author, tradition…) | |
| plus a sample page rendering + ingest cost estimate. | |
| 3. ``POST /books`` body :class:`CreateBookRequest` (in ``dto/books.py``) | |
| → :class:`BookDTO` — commits the row and moves the upload bytes into | |
| the storage backend at ``data/raw/{bookId}.pdf``. | |
| Every DTO inherits :class:`ApiModel`. Enums (extraction mode, era, | |
| tradition, language) reuse :mod:`src.api.dto.books` so the same TS union | |
| types the rest of the library uses are reused on the FE. | |
| """ | |
| from __future__ import annotations | |
| from pydantic import Field | |
| from .books import ( | |
| EraEnum, | |
| ExtractionModeEnum, | |
| LanguageEnum, | |
| ReligionEnum, | |
| TraditionEnum, | |
| ) | |
| from .common import ApiModel | |
| class UploadResponse(ApiModel): | |
| """Body for ``POST /uploads`` — multipart-PDF upload. | |
| The temp file lands at ``data/uploads/{uploadId}.pdf`` keyed by a | |
| fresh uuid (``upload_id``). The SHA-256 is computed server-side as a | |
| side effect of writing the bytes — the FE uses it as the cache key | |
| for probe results so re-uploading the same file doesn't re-bill the | |
| Gemini probe call. | |
| """ | |
| upload_id: str | |
| sha256: str | |
| size_bytes: int | |
| class ProbeRequest(ApiModel): | |
| """Body for ``POST /books/probe`` — at least one of the two must be set. | |
| The router validates "at least one" at runtime (raises | |
| ``BAD_REQUEST`` otherwise) so the wire schema can stay simple — both | |
| fields being optional matches the FE's typical flow where the user | |
| is editing either a URL or a freshly uploaded file. | |
| """ | |
| source_url: str | None = None | |
| upload_id: str | None = None | |
| class GuessedMetadata(ApiModel): | |
| """The metadata fields the probe attempts to fill in from the PDF. | |
| Every field is ``Optional`` — when Gemini can't extract a confident | |
| value, the field stays ``None`` and the wizard renders the input | |
| empty. Religion fields default to ``None`` because the probe is most | |
| confident about title/author and least confident about religion of | |
| the author (which sometimes requires external knowledge the model | |
| doesn't have). | |
| """ | |
| title_ar: str | None = None | |
| title_en: str | None = None | |
| author: str | None = None | |
| author_id: str | None = None | |
| era: EraEnum | None = None | |
| tradition: TraditionEnum | None = None | |
| language: LanguageEnum | None = None | |
| book_religion: ReligionEnum | None = None | |
| author_religion: ReligionEnum | None = None | |
| confidence: str | None = None | |
| rationale: str | None = None | |
| class SuggestedLabel(ApiModel): | |
| """One label the probe thinks the user might want to apply. | |
| The ``id`` is the slug Gemini returned (canonicalised to lowercase | |
| and trimmed to <=64 chars in | |
| :func:`src.lib.metadata_probe._clean_suggested_labels`). The wizard | |
| shows them as togglable chips; ticked chips become | |
| ``CreateBookRequest.label_ids`` on submit. The label rows themselves | |
| are created lazily by the create endpoint (or by | |
| ``POST /labels/seed-from-derived``). | |
| """ | |
| id: str | |
| reason: str | None = None | |
| class SamplePage(ApiModel): | |
| """One PDF page rendered to text for the wizard's "sanity check" pane. | |
| For native-text PDFs ``ocr_text`` is the PyMuPDF extraction (free, | |
| no API call). For scanned PDFs the value is whatever the probe | |
| surfaced — which may be empty if the probe didn't sample image pages. | |
| ``clean_text`` is unset on probe (Stage 4 hasn't run); included in | |
| the DTO so the same shape can be reused for ``GET /books/.../pages`` | |
| later. | |
| """ | |
| pdf_page: int | |
| ocr_text: str | |
| clean_text: str | None = None | |
| class ProbeResponse(ApiModel): | |
| """The Add Book wizard's pre-flight result. | |
| The FE uses this to render Step 2 of the wizard: prefilled form | |
| + suggested labels + sample pages + a hard cost number for the | |
| "Run ingest" CTA. ``estimated_ingest_cost_usd`` is the | |
| counterfactual cost of running the full pipeline at the suggested | |
| extraction mode, computed against ``tools_registry.yaml`` so it | |
| moves with pricing updates. | |
| """ | |
| pages_total: int | |
| has_text_layer: bool | |
| suggested_extraction_mode: ExtractionModeEnum | |
| guessed_metadata: GuessedMetadata | |
| suggested_labels: list[SuggestedLabel] = Field(default_factory=list) | |
| sample_pages: list[SamplePage] = Field(default_factory=list) | |
| estimated_ingest_cost_usd: float | |
| probe_error: str | None = None | |