hasari-api / services /backend /models.py
erdoganpeker's picture
v0.3.0 — multimodal vehicle damage MVP
e327f0d
"""
backend/models.py
Pydantic v2 semalari — packages/types/src ile birebir senkron.
Onemli: Frontend (web/mobile/desktop) tum tipleri packages/types'tan import eder.
Bu dosyada degisiklik yaptiginda TypeScript tarafini da guncelle.
"""
from __future__ import annotations
from typing import Any, Dict, List, Literal, Optional, Tuple
from pydantic import BaseModel, ConfigDict, EmailStr, Field
# ---------------- Yardimcilar ----------------
# Tum modeller "ekstra alana izin verme" yi tercih eder — frontend ile sozlesme net olsun
StrictModel = ConfigDict(extra="forbid", populate_by_name=True)
# Bazi durumlar (ML output, ileri uyumluluk) icin esnek model
LooseModel = ConfigDict(extra="allow", populate_by_name=True)
# ---------------- Enum-benzeri Literal'lar (packages/types ile ayni) ----------------
DamageType = Literal[
"dent",
"scratch",
"crack",
"glass_shatter",
"lamp_broken",
"tire_flat",
]
SeverityLevel = Literal["hafif", "orta", "agir"]
CostConfidence = Literal["high", "medium", "low"]
# Pipeline severity classifier 'ensemble_resolved' (rule + CNN ensemble, sonra
# conflict resolution) ve 'rule_based' adlarini da donduruyor; geriye uyumluluk
# icin tum varyantlari kabul ediyoruz.
SeverityMethod = Literal[
"rule",
"rule_based",
"cnn",
"ensemble",
"ensemble_resolved",
]
PartName = Literal[
"front_bumper",
"back_bumper",
"hood",
"front_glass",
"back_glass",
"front_left_door",
"front_right_door",
"back_left_door",
"back_right_door",
"front_left_light",
"front_right_light",
"front_light",
"back_left_light",
"back_right_light",
"back_light",
"left_mirror",
"right_mirror",
"tailgate",
"trunk",
"wheel",
"back_door",
"unknown",
]
PartStatus = Literal["clean", "minor_damage", "moderate_damage", "severe_damage"]
RepairRecommendation = Literal[
"kucuk_tamir",
"tamir_boya",
"parca_degisimi",
"agir_hasar_pert_degerlendirme",
"hasar_yok",
]
InspectionStatus = Literal["queued", "processing", "completed", "failed"]
# ---------------- Damage ----------------
class SeverityResult(BaseModel):
model_config = StrictModel
level: SeverityLevel
level_tr: str
confidence: float = Field(ge=0.0, le=1.0)
method: SeverityMethod
class CostEstimate(BaseModel):
model_config = StrictModel
min_tl: float = Field(ge=0.0)
max_tl: float = Field(ge=0.0)
midpoint_tl: Optional[float] = None
confidence: CostConfidence
source: str
class Damage(BaseModel):
"""Tek bir hasar kaydi — packages/types/src/damage.ts::Damage ile ayni."""
model_config = LooseModel # ML extra alan ekleyebilir (source_image vs)
id: int
type: DamageType
type_tr: str
confidence: float = Field(ge=0.0, le=1.0)
severity: SeverityResult
bbox: Tuple[float, float, float, float]
polygon_normalized: List[List[float]] = []
area_ratio: float = Field(ge=0.0, le=1.0)
cost: CostEstimate
is_multi_part: bool = False
is_low_confidence_match: bool = False
affected_parts: Optional[List[str]] = None
# ---------------- Part ----------------
class Part(BaseModel):
"""Parca-merkezli kayit — packages/types/src/part.ts::Part ile ayni."""
model_config = LooseModel
name: str # PartName veya unbekannt — string union; TS tarafi `PartName | string`
name_tr: str
confidence: float = Field(ge=0.0, le=1.0)
status: PartStatus
damage_count: int = Field(ge=0)
polygon_normalized: List[List[float]] = []
bbox: Tuple[float, float, float, float]
damages: List[Damage] = []
part_cost_min_tl: float = Field(default=0.0, ge=0.0)
part_cost_max_tl: float = Field(default=0.0, ge=0.0)
cost_note: Optional[str] = None
# ---------------- Inspection ----------------
class InspectionSummary(BaseModel):
model_config = StrictModel
total_parts_inspected: int = Field(ge=0)
damaged_parts_count: int = Field(ge=0)
clean_parts_count: int = Field(ge=0)
total_damage_count: int = Field(ge=0)
unknown_part_damages_count: int = Field(ge=0)
multi_part_damages_count: int = Field(ge=0)
most_severe_level: Optional[SeverityLevel] = None
most_severe_level_tr: Optional[str] = None
total_damage_area_ratio: float = Field(ge=0.0)
total_cost_range_tl: Tuple[float, float] = (0.0, 0.0)
total_cost_midpoint_tl: Optional[float] = None
cost_confidence: CostConfidence = "low"
repair_recommendation: RepairRecommendation = "hasar_yok"
repair_recommendation_tr: str = "Hasar tespit edilmedi"
estimated_repair_days: int = Field(default=0, ge=0)
class VisualizationUrls(BaseModel):
model_config = StrictModel
annotated: Optional[str] = None
parts: Optional[str] = None
damages: Optional[str] = None
class InspectionImage(BaseModel):
model_config = StrictModel
# ge=0 olarak biraktik: aggregate_results bos sonuc icin (0,0) iskeleti uretebiliyor.
width: int = Field(ge=0)
height: int = Field(ge=0)
url: Optional[str] = None
hash: Optional[str] = None
class Inspection(BaseModel):
"""Bir incelemenin ana sonuc DTO'su."""
model_config = LooseModel # ML 'damages_raw', 'parts_detected' gibi ekstra alan ekleyebilir
inspection_id: str
timestamp: str
image: InspectionImage
parts: List[Part] = []
summary: InspectionSummary
multi_part_damages: Optional[List[Damage]] = None
unassigned_damages: Optional[List[Damage]] = None
visualization_urls: Optional[VisualizationUrls] = None
# ---------------- API responses ----------------
class HealthResponse(BaseModel):
model_config = StrictModel
status: Literal["ok", "degraded", "down"]
ml_loaded: bool
timestamp: str
version: Optional[str] = None
class VersionResponse(BaseModel):
model_config = StrictModel
version: str
git_sha: str
build_time: str
environment: str
class InspectionCreateResponse(BaseModel):
model_config = StrictModel
inspection_id: str
status: InspectionStatus
status_url: str
created_at: str
estimated_completion_seconds: Optional[int] = 30
class InspectionStatusResponse(BaseModel):
model_config = StrictModel
inspection_id: str
status: InspectionStatus
result: Optional[Inspection] = None
error: Optional[str] = None
created_at: str
completed_at: Optional[str] = None
class SyncInspectionResponse(BaseModel):
model_config = StrictModel
inspection_id: str
result: Inspection
processed_at: str
class ApiError(BaseModel):
model_config = StrictModel
detail: str
code: Optional[str] = None
class InspectionListItem(BaseModel):
model_config = StrictModel
inspection_id: str
created_at: str
status: InspectionStatus
damage_count: int = Field(ge=0)
total_cost_midpoint_tl: Optional[float] = None
thumbnail_url: Optional[str] = None
class InspectionListResponse(BaseModel):
model_config = StrictModel
items: List[InspectionListItem]
total: int = Field(ge=0)
page: int = Field(ge=1)
page_size: int = Field(ge=1, le=200)
# ---------------- Auth ----------------
class UserRegisterRequest(BaseModel):
model_config = StrictModel
email: EmailStr
password: str = Field(min_length=8, max_length=128)
full_name: Optional[str] = Field(default=None, max_length=120)
class UserLoginRequest(BaseModel):
model_config = StrictModel
email: EmailStr
password: str = Field(min_length=1, max_length=128)
class TokenPair(BaseModel):
model_config = StrictModel
access_token: str
refresh_token: str
token_type: Literal["bearer"] = "bearer"
expires_in: int # access token TTL (sn)
class RefreshTokenRequest(BaseModel):
model_config = StrictModel
refresh_token: str = Field(min_length=10)
class UserPublic(BaseModel):
"""Kullaniciya geri donen / /auth/me icin guvenli (PII'siz hash icermez) user kaydi."""
model_config = StrictModel
id: str
email: EmailStr
full_name: Optional[str] = None
role: Literal["user", "admin"] = "user"
is_active: bool = True
created_at: str
# ---------------- WebSocket mesajlari ----------------
class WSStatusMessage(BaseModel):
model_config = StrictModel
type: Literal["status"] = "status"
inspection_id: str
status: InspectionStatus
progress: Optional[float] = None # 0.0 - 1.0
class WSCompletedMessage(BaseModel):
model_config = StrictModel
type: Literal["completed"] = "completed"
inspection_id: str
result: Inspection
class WSErrorMessage(BaseModel):
model_config = StrictModel
type: Literal["error"] = "error"
inspection_id: str
error: str