84 lines
3.3 KiB
Python
84 lines
3.3 KiB
Python
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"]),
|
||
]
|