Imago/app/models/image.py

98 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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
)
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) # nom sur disque (uuid-based)
file_path = Column(String(1024), nullable=False)
thumbnail_path = Column(String(1024))
mime_type = Column(String(128))
file_size = Column(BigInteger) # bytes
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) # dict complet brut
exif_make = Column(String(256)) # Appareil — fabricant
exif_model = Column(String(256)) # Appareil — modèle
exif_lens = Column(String(256))
exif_taken_at = Column(DateTime(timezone=True)) # DateTimeOriginal EXIF
exif_gps_lat = Column(Float)
exif_gps_lon = Column(Float)
exif_altitude = Column(Float)
exif_iso = Column(Integer)
exif_aperture = Column(String(32)) # ex: "f/2.8"
exif_shutter = Column(String(32)) # ex: "1/250"
exif_focal = Column(String(32)) # ex: "50mm"
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) # 0.0 1.0
ocr_has_text = Column(Boolean, default=False)
# ── AI Vision ─────────────────────────────────────────────
ai_description = Column(Text)
ai_tags = Column(JSON) # ["nature", "paysage", ...]
ai_confidence = Column(Float) # score de confiance global
ai_model_used = Column(String(128))
ai_processed_at = Column(DateTime(timezone=True))
ai_prompt_tokens = Column(Integer)
ai_output_tokens = Column(Integer)
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