Files
Imago/app/models/image.py
T

106 lines
4.5 KiB
Python

"""
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"<Image id={self.id} name={self.original_name} status={self.processing_status}>"
@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