import uuid from django.contrib.gis.db import models from django.conf import settings class Splat(models.Model): class Status(models.TextChoices): PENDING = "pending", "Pending" # created, awaiting video upload confirmation PROCESSING = "processing", "Processing" # RunPod job running READY = "ready", "Ready" # pipeline done, quality check passed FAILED = "failed", "Failed" # pipeline error or quality check failed id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="splats" ) challenge = models.ForeignKey( "challenges.Challenge", null=True, blank=True, on_delete=models.SET_NULL, related_name="splats", ) status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING, db_index=True) # Visible on the map only after pipeline succeeds and quality gate passes is_published = models.BooleanField(default=False, db_index=True) # --- Geo fields --- # Anchor point (centroid) used for map pin and proximity queries location = models.PointField(geography=True, null=True, blank=True) # Footprint polygon of the splat's coverage on the ground coverage = models.PolygonField(geography=True, null=True, blank=True) # Compass bearing of the camera at recording start (0–360°), used to orient # the splat when rendering it on the map heading = models.FloatField(null=True, blank=True) # Elevation above sea level in metres, for Cesium 3D positioning altitude = models.FloatField(null=True, blank=True) # --- Wasabi storage keys --- # Set at Splat creation so the presigned upload URL can be generated immediately. # splat_key and preview_key are populated by the pipeline on completion. video_key = models.CharField(max_length=500, blank=True, default="") splat_key = models.CharField(max_length=500, blank=True, default="") preview_key = models.CharField(max_length=500, blank=True, default="") splat_file_size = models.PositiveBigIntegerField(null=True, blank=True) # bytes # Pipeline output quality signals quality_score = models.FloatField(null=True, blank=True) # 0.0–1.0 frame_count = models.PositiveIntegerField(null=True, blank=True) # Per-frame GPS/IMU data from Vision Camera, passed as-is to the pipeline. # Expected shape: # { # "fps": 30, # "duration_seconds": 45.2, # "device_model": "iPhone 15 Pro", # "frames": [ # { # "timestamp": 0.033, # "lat": 52.520008, "lon": 13.404954, # "altitude_m": 34.2, # "heading_deg": 178.3, "pitch_deg": -12.5, "roll_deg": 2.1, # "accuracy_m": 3.0 # }, ... # ] # } capture_metadata = models.JSONField(default=dict, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return f"Splat {self.id} [{self.status}] by {self.owner_id}" class Meta: db_table = "splats" indexes = [ models.Index(fields=["owner", "status"]), models.Index(fields=["challenge", "is_published"]), models.Index(fields=["created_at"]), ]