""" Modèle SQLAlchemy — Image et métadonnées associées """ import enum from datetime import datetime, timezone from sqlalchemy import ( Column, Integer, String, Text, DateTime, JSON, Float, Enum as SAEnum, BigInteger, Boolean, ForeignKey, Index ) from sqlalchemy.orm import relationship from app.database import Base class ProcessingStatus(str, enum.Enum): PENDING = "pending" PROCESSING = "processing" DONE = "done" ERROR = "error" class Image(Base): __tablename__ = "images" # ── Identité ────────────────────────────────────────────── id = Column(Integer, primary_key=True, index=True) uuid = Column(String(36), unique=True, index=True, nullable=False) # ── Client (multi-tenant) ───────────────────────────────── client_id = Column(String(36), ForeignKey("api_clients.id"), nullable=False, index=True) client = relationship("APIClient", back_populates="images") # ── Fichier ─────────────────────────────────────────────── original_name = Column(String(512), nullable=False) filename = Column(String(512), nullable=False) file_path = Column(String(1024), nullable=False) thumbnail_path = Column(String(1024)) mime_type = Column(String(128)) file_size = Column(BigInteger) width = Column(Integer) height = Column(Integer) uploaded_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) # ── Statut du pipeline AI ───────────────────────────────── processing_status = Column( SAEnum(ProcessingStatus), default=ProcessingStatus.PENDING, nullable=False, index=True ) processing_error = Column(Text) processing_started_at = Column(DateTime(timezone=True)) processing_done_at = Column(DateTime(timezone=True)) # ── Métadonnées EXIF ────────────────────────────────────── exif_raw = Column(JSON) exif_make = Column(String(256)) exif_model = Column(String(256)) exif_lens = Column(String(256)) exif_taken_at = Column(DateTime(timezone=True)) exif_gps_lat = Column(Float) exif_gps_lon = Column(Float) exif_altitude = Column(Float) exif_iso = Column(Integer) exif_aperture = Column(String(32)) exif_shutter = Column(String(32)) exif_focal = Column(String(32)) exif_flash = Column(Boolean) exif_orientation = Column(Integer) exif_software = Column(String(256)) # ── OCR ─────────────────────────────────────────────────── ocr_text = Column(Text) ocr_language = Column(String(64)) ocr_confidence = Column(Float) ocr_has_text = Column(Boolean, default=False) # ── AI Vision ───────────────────────────────────────────── ai_description = Column(Text) ai_tags = Column(JSON) ai_confidence = Column(Float) ai_model_used = Column(String(128)) ai_processed_at = Column(DateTime(timezone=True)) ai_prompt_tokens = Column(Integer) ai_output_tokens = Column(Integer) # ── Indexes composites ──────────────────────────────────── __table_args__ = ( # Pour les listings paginés par client (requête la plus fréquente) Index("ix_images_client_uploaded", "client_id", "uploaded_at"), # Pour le filtrage par statut + client Index("ix_images_client_status", "client_id", "processing_status"), ) def __repr__(self): return f"" @property def has_gps(self) -> bool: return self.exif_gps_lat is not None and self.exif_gps_lon is not None @property def dimensions(self) -> str | None: if self.width and self.height: return f"{self.width}x{self.height}" return None