rcnn/backend/apps/splats/models.py
Marius Unsel d93412cd0d Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 01:12:40 +02:00

84 lines
3.3 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.

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 (0360°), 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.01.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"]),
]