feat: add AirShelf core implementation
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
from django.db import models
|
||||
|
||||
from apps.common.models import TeamOwnedModel, TimeStampedModel
|
||||
|
||||
|
||||
class Project(TeamOwnedModel):
|
||||
class Status(models.TextChoices):
|
||||
DRAFT = "draft", "Draft"
|
||||
SCRIPTING = "scripting", "Scripting"
|
||||
ASSETING = "asseting", "Asseting"
|
||||
STORYBOARDING = "storyboarding", "Storyboarding"
|
||||
VIDEOING = "videoing", "Videoing"
|
||||
EXPORTING = "exporting", "Exporting"
|
||||
COMPLETED = "completed", "Completed"
|
||||
FAILED = "failed", "Failed"
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
product = models.ForeignKey("products.Product", on_delete=models.PROTECT, related_name="projects")
|
||||
status = models.CharField(max_length=32, choices=Status.choices, default=Status.DRAFT)
|
||||
current_stage = models.CharField(max_length=32, default="script")
|
||||
budget_limit = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
||||
failure_reason = models.TextField(blank=True)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["team", "status"]),
|
||||
models.Index(fields=["team", "current_stage"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class ProjectStage(TimeStampedModel):
|
||||
class Stage(models.TextChoices):
|
||||
SCRIPT = "script", "Script"
|
||||
BASE_ASSETS = "base_assets", "Base Assets"
|
||||
STORYBOARD = "storyboard", "Storyboard"
|
||||
VIDEO = "video", "Video"
|
||||
EXPORT = "export", "Export"
|
||||
|
||||
class Status(models.TextChoices):
|
||||
NOT_STARTED = "not_started", "Not Started"
|
||||
DRAFT = "draft", "Draft"
|
||||
QUEUED = "queued", "Queued"
|
||||
RUNNING = "running", "Running"
|
||||
SUCCEEDED = "succeeded", "Succeeded"
|
||||
FAILED = "failed", "Failed"
|
||||
SKIPPED = "skipped", "Skipped"
|
||||
NEEDS_REVIEW = "needs_review", "Needs Review"
|
||||
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="stages")
|
||||
stage = models.CharField(max_length=32, choices=Stage.choices)
|
||||
status = models.CharField(max_length=32, choices=Status.choices, default=Status.NOT_STARTED)
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
error_message = models.TextField(blank=True)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = [("project", "stage")]
|
||||
ordering = ["created_at"]
|
||||
|
||||
|
||||
class ScriptVersion(TimeStampedModel):
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="script_versions")
|
||||
task = models.ForeignKey("ai.AITask", on_delete=models.SET_NULL, null=True, blank=True, related_name="script_versions")
|
||||
title = models.CharField(max_length=128, blank=True)
|
||||
content = models.TextField()
|
||||
source = models.CharField(max_length=32, default="ai")
|
||||
is_adopted = models.BooleanField(default=False)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
|
||||
|
||||
class ScriptSegment(TimeStampedModel):
|
||||
script_version = models.ForeignKey(ScriptVersion, on_delete=models.CASCADE, related_name="segments")
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
duration_seconds = models.PositiveIntegerField(default=15)
|
||||
narration = models.TextField(blank=True)
|
||||
visual_prompt = models.TextField(blank=True)
|
||||
product_points = models.JSONField(default=list, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["sort_order", "created_at"]
|
||||
|
||||
|
||||
class BaseAssetGroup(TimeStampedModel):
|
||||
class Kind(models.TextChoices):
|
||||
PRODUCT = "product", "Product"
|
||||
PERSON = "person", "Person"
|
||||
SCENE = "scene", "Scene"
|
||||
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="base_asset_groups")
|
||||
kind = models.CharField(max_length=32, choices=Kind.choices)
|
||||
task = models.ForeignKey("ai.AITask", on_delete=models.SET_NULL, null=True, blank=True, related_name="base_asset_groups")
|
||||
prompt = models.TextField(blank=True)
|
||||
adopted_asset = models.ForeignKey(
|
||||
"assets.Asset",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="adopted_base_groups",
|
||||
)
|
||||
candidate_assets = models.ManyToManyField("assets.Asset", blank=True, related_name="candidate_base_groups")
|
||||
version = models.PositiveIntegerField(default=1)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [models.Index(fields=["project", "kind"])]
|
||||
|
||||
|
||||
class StoryboardVersion(TimeStampedModel):
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="storyboard_versions")
|
||||
task = models.ForeignKey(
|
||||
"ai.AITask",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="storyboard_versions",
|
||||
)
|
||||
prompt = models.TextField(blank=True)
|
||||
is_adopted = models.BooleanField(default=False)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
|
||||
|
||||
class StoryboardFrame(TimeStampedModel):
|
||||
storyboard = models.ForeignKey(StoryboardVersion, on_delete=models.CASCADE, related_name="frames")
|
||||
script_segment = models.ForeignKey(
|
||||
ScriptSegment,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="storyboard_frames",
|
||||
)
|
||||
asset = models.ForeignKey("assets.Asset", on_delete=models.PROTECT, related_name="storyboard_frames")
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
prompt = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["sort_order", "created_at"]
|
||||
|
||||
|
||||
class VideoSegment(TimeStampedModel):
|
||||
class Status(models.TextChoices):
|
||||
NOT_STARTED = "not_started", "Not Started"
|
||||
QUEUED = "queued", "Queued"
|
||||
RUNNING = "running", "Running"
|
||||
SUCCEEDED = "succeeded", "Succeeded"
|
||||
FAILED = "failed", "Failed"
|
||||
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="video_segments")
|
||||
script_segment = models.ForeignKey(
|
||||
ScriptSegment,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="video_segments",
|
||||
)
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
target_duration_seconds = models.PositiveIntegerField(default=15)
|
||||
status = models.CharField(max_length=32, choices=Status.choices, default=Status.NOT_STARTED)
|
||||
adopted_version = models.ForeignKey(
|
||||
"projects.VideoSegmentVersion",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="adopted_by_segments",
|
||||
)
|
||||
error_message = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = [("project", "sort_order")]
|
||||
ordering = ["sort_order", "created_at"]
|
||||
|
||||
|
||||
class VideoSegmentVersion(TimeStampedModel):
|
||||
video_segment = models.ForeignKey(VideoSegment, on_delete=models.CASCADE, related_name="versions")
|
||||
task = models.ForeignKey("ai.AITask", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_versions")
|
||||
asset = models.ForeignKey("assets.Asset", on_delete=models.PROTECT, related_name="video_segment_versions")
|
||||
prompt = models.TextField(blank=True)
|
||||
is_adopted = models.BooleanField(default=False)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
|
||||
|
||||
class Timeline(TimeStampedModel):
|
||||
project = models.OneToOneField(Project, on_delete=models.CASCADE, related_name="timeline")
|
||||
name = models.CharField(max_length=255, blank=True)
|
||||
aspect_ratio = models.CharField(max_length=16, default="9:16")
|
||||
resolution = models.CharField(max_length=32, default="1080x1920")
|
||||
duration_seconds = models.PositiveIntegerField(default=60)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
|
||||
|
||||
class TimelineClip(TimeStampedModel):
|
||||
timeline = models.ForeignKey(Timeline, on_delete=models.CASCADE, related_name="clips")
|
||||
asset = models.ForeignKey("assets.Asset", on_delete=models.PROTECT, related_name="timeline_clips")
|
||||
sort_order = models.PositiveIntegerField(default=0)
|
||||
start_ms = models.PositiveIntegerField(default=0)
|
||||
duration_ms = models.PositiveIntegerField(default=15000)
|
||||
trim_start_ms = models.PositiveIntegerField(default=0)
|
||||
trim_end_ms = models.PositiveIntegerField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["sort_order", "created_at"]
|
||||
|
||||
|
||||
class SubtitleTrack(TimeStampedModel):
|
||||
timeline = models.ForeignKey(Timeline, on_delete=models.CASCADE, related_name="subtitle_tracks")
|
||||
content = models.JSONField(default=list, blank=True)
|
||||
style = models.JSONField(default=dict, blank=True)
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
|
||||
class BgmTrack(TimeStampedModel):
|
||||
timeline = models.ForeignKey(Timeline, on_delete=models.CASCADE, related_name="bgm_tracks")
|
||||
asset = models.ForeignKey("assets.Asset", on_delete=models.PROTECT, related_name="bgm_tracks")
|
||||
volume = models.PositiveIntegerField(default=60)
|
||||
start_ms = models.PositiveIntegerField(default=0)
|
||||
|
||||
|
||||
class ExportJob(TimeStampedModel):
|
||||
class Status(models.TextChoices):
|
||||
DRAFT = "draft", "Draft"
|
||||
QUEUED = "queued", "Queued"
|
||||
RUNNING = "running", "Running"
|
||||
SUCCEEDED = "succeeded", "Succeeded"
|
||||
FAILED = "failed", "Failed"
|
||||
|
||||
timeline = models.ForeignKey(Timeline, on_delete=models.CASCADE, related_name="export_jobs")
|
||||
status = models.CharField(max_length=32, choices=Status.choices, default=Status.DRAFT)
|
||||
task = models.ForeignKey("ai.AITask", on_delete=models.SET_NULL, null=True, blank=True, related_name="export_jobs")
|
||||
output_asset = models.ForeignKey("assets.Asset", on_delete=models.SET_NULL, null=True, blank=True, related_name="export_jobs")
|
||||
progress = models.PositiveIntegerField(default=0)
|
||||
error_message = models.TextField(blank=True)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
|
||||
Reference in New Issue
Block a user