from __future__ import annotations import time import uuid from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field class _StrictModel(BaseModel): model_config = ConfigDict(strict=True, extra="forbid") class BoundingBox(_StrictModel): """Page-normalised bounding box (all coords 0..1, relative to page element).""" page: int x: float y: float w: float h: float class SelectionSnippet(_StrictModel): page_number: int text: str boxes: List[BoundingBox] char_start: Optional[int] = None char_end: Optional[int] = None class StudentAnnotation(_StrictModel): annotation_id: str = Field(default_factory=lambda: str(uuid.uuid4())) document_id: str = Field(pattern=r"^[0-9a-f]{64}$") project_id: str = Field(pattern=r"^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$") target_snippets: List[SelectionSnippet] note_text: Optional[str] = None image_base64: Optional[str] = None # cropped region PNG (pinned figures/tables/formulas) created_at: float = Field(default_factory=time.time) updated_at: float = Field(default_factory=time.time)