106 lines
4.5 KiB
Python
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
|