243 lines
11 KiB
Python
243 lines
11 KiB
Python
from django.db import models
|
|
|
|
from apps.common.models import TeamOwnedModel, TimeStampedModel
|
|
|
|
|
|
class Asset(TeamOwnedModel):
|
|
class Type(models.TextChoices):
|
|
IMAGE = "image", "Image"
|
|
VIDEO = "video", "Video"
|
|
AUDIO = "audio", "Audio"
|
|
SUBTITLE = "subtitle", "Subtitle"
|
|
DOCUMENT = "document", "Document"
|
|
|
|
class Source(models.TextChoices):
|
|
UPLOAD = "upload", "Upload"
|
|
AI_GENERATED = "ai_generated", "AI Generated"
|
|
EXPORTED = "exported", "Exported"
|
|
SYSTEM = "system", "System"
|
|
|
|
class Category(models.TextChoices):
|
|
PERSON = "person", "Person" # 视频趴·角色定妆照(送审)
|
|
SCENE = "scene", "Scene"
|
|
PRODUCT_IMAGE = "product_image", "Product Image"
|
|
VIDEO_CLIP = "video_clip", "Video Clip"
|
|
FINAL_VIDEO = "final_video", "Final Video"
|
|
UPLOAD = "upload", "Upload"
|
|
UNCATEGORIZED = "uncategorized", "Uncategorized"
|
|
# ↓ 模特库 + 资产模型重构(2026-06-20)新增,逐期接线。
|
|
MODEL_PORTRAIT = "model_portrait", "Model Portrait" # 模特形象图(图片域·不送审)
|
|
TRI_VIEW = "tri_view", "Tri View" # 三视图(送审)
|
|
VOICE = "voice", "Voice" # 模特声线(占位·尾期)
|
|
MODEL_TRYON = "model_tryon", "Model Try-on" # 模特上身图(图片趴·不送审)
|
|
PLATFORM_KIT = "platform_kit", "Platform Kit" # 平台套图(图片趴)
|
|
FREE_CREATE = "free_create", "Free Create" # 自由创作(图片趴)
|
|
STORYBOARD = "storyboard", "Storyboard" # 分镜图(视频趴·送审)
|
|
|
|
# 送审范围(火山人像审核):视频趴含人脸 = 角色定妆照 / 模特形象图 / 三视图 / 分镜图 / 场景 / 商品图。
|
|
# model_portrait(模特库形象图,尤其真人上传)与 person 同样是真人脸,必须可送审——否则新增模特后在
|
|
# 基础资产点「待审核」会被判「该素材无需审核」(ZWQ#6:AI 角色 person 可审、上传模特 model_portrait 报无需审核)。
|
|
# 场景图与商品图也可能出现真人(模特出镜/上身),不送审则视频路只能传原始直链 → 火山判「疑似真人」拒;
|
|
# 送审后拿到 remote_id,视频路换 asset:// 素材库引用即可放行。图片趴(上身图/套图/创作)仍不送审。
|
|
REVIEW_CATEGORIES = ("person", "model_portrait", "tri_view", "storyboard", "scene", "product_image")
|
|
|
|
name = models.CharField(max_length=255)
|
|
asset_type = models.CharField(max_length=24, choices=Type.choices)
|
|
source = models.CharField(max_length=32, choices=Source.choices)
|
|
category = models.CharField(max_length=32, choices=Category.choices, default=Category.UNCATEGORIZED)
|
|
description = models.TextField(blank=True)
|
|
origin_task = models.ForeignKey(
|
|
"ai.AITask",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="generated_assets",
|
|
)
|
|
metadata = models.JSONField(default=dict, blank=True)
|
|
is_deleted = models.BooleanField(default=False)
|
|
purged_at = models.DateTimeField(null=True, blank=True)
|
|
# 是否进「资产库」列表:工作台生成的图(模特上身图/平台套图/自由创作)默认 False(只在工作台展示),
|
|
# 用户「加入资产库」后才置 True 出现在资产库列表;「取消加入」置回 False 即从列表移出。
|
|
# 字段默认 True 是为了让历史数据 / 上传 / 项目内资产保持原样可见(只对新生成的工作台图显式置 False)。
|
|
in_library = models.BooleanField(default=True, db_default=True)
|
|
# 火山人像素材库审核(仅真人 person 资产):"" = 未送审 / processing 审核中 / active 绿盾通过 / failed 红标(需改提示词重生)
|
|
review_status = models.CharField(max_length=16, blank=True, db_default="")
|
|
review_remote_id = models.CharField(max_length=128, blank=True, db_default="")
|
|
review_error = models.TextField(blank=True, null=True, default="") # TEXT 不能有 DB 默认值(MySQL 1101),改用可空容忍「漏带字段」的插入
|
|
|
|
class Meta:
|
|
indexes = [
|
|
# 资产库按团队 + 分类倒序时间翻页(products 详情素材趴 / 资产库)
|
|
models.Index(fields=["team", "category", "-created_at"]),
|
|
models.Index(fields=["team", "asset_type"]),
|
|
models.Index(fields=["team", "is_deleted", "purged_at"]),
|
|
]
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
|
|
class Model(TeamOwnedModel):
|
|
"""模特(顶级实体 = 模特库)· 团队级、可复用。
|
|
|
|
一个完整模特 = 形象图 + 三视图 + 声线(声线尾期做)。图片线(上身图)+ 视频线(角色)都能引用它。
|
|
来源:官方预制(is_official=True,打「官方模板」标签)/ 商家自建(AI 生成或真人上传)。
|
|
portrait/triview/voice 指向具体 Asset(复用资产表,不重复存文件)。
|
|
"""
|
|
|
|
class Source(models.TextChoices):
|
|
AI = "ai", "AI Generated"
|
|
UPLOAD = "upload", "Upload"
|
|
|
|
name = models.CharField(max_length=255)
|
|
is_official = models.BooleanField(default=False) # 官方模板标签
|
|
source = models.CharField(max_length=16, choices=Source.choices, default=Source.AI)
|
|
portrait_asset = models.ForeignKey(
|
|
Asset, on_delete=models.SET_NULL, null=True, blank=True, related_name="model_as_portrait"
|
|
)
|
|
triview_asset = models.ForeignKey(
|
|
Asset, on_delete=models.SET_NULL, null=True, blank=True, related_name="model_as_triview"
|
|
)
|
|
voice_asset = models.ForeignKey( # 声线,尾期接线,先留空
|
|
Asset, on_delete=models.SET_NULL, null=True, blank=True, related_name="model_as_voice"
|
|
)
|
|
description = models.TextField(blank=True)
|
|
metadata = models.JSONField(default=dict, blank=True)
|
|
is_deleted = models.BooleanField(default=False)
|
|
purged_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
indexes = [
|
|
# 模特库默认按团队 + 新→旧;官方模板优先靠前由序列化/查询控制
|
|
models.Index(fields=["team", "is_official", "-created_at"]),
|
|
models.Index(fields=["team", "is_deleted", "purged_at"]),
|
|
]
|
|
ordering = ["-created_at"]
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
|
|
class AssetReviewGroup(TimeStampedModel):
|
|
"""一团队一火山人像素材组(真人资产审核统一上传到这里,单组可放 500 万)。"""
|
|
|
|
team = models.OneToOneField("accounts.Team", on_delete=models.CASCADE, related_name="review_group")
|
|
remote_group_id = models.CharField(max_length=128)
|
|
name = models.CharField(max_length=128, blank=True)
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.team_id}:{self.remote_group_id}"
|
|
|
|
|
|
class AssetFile(TimeStampedModel):
|
|
asset = models.ForeignKey(Asset, on_delete=models.CASCADE, related_name="files")
|
|
object_key = models.CharField(max_length=512)
|
|
bucket = models.CharField(max_length=128)
|
|
content_type = models.CharField(max_length=128, blank=True)
|
|
size_bytes = models.BigIntegerField(default=0)
|
|
checksum = models.CharField(max_length=128, blank=True)
|
|
width = models.PositiveIntegerField(null=True, blank=True)
|
|
height = models.PositiveIntegerField(null=True, blank=True)
|
|
duration_ms = models.PositiveIntegerField(null=True, blank=True)
|
|
preview_url = models.URLField(blank=True)
|
|
is_primary = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(fields=["bucket", "object_key"]),
|
|
]
|
|
|
|
def __str__(self) -> str:
|
|
return self.object_key
|
|
|
|
|
|
class AssetTag(TimeStampedModel):
|
|
team = models.ForeignKey("accounts.Team", on_delete=models.CASCADE, related_name="asset_tags")
|
|
name = models.CharField(max_length=64)
|
|
|
|
class Meta:
|
|
unique_together = [("team", "name")]
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
|
|
class AssetTagging(TimeStampedModel):
|
|
asset = models.ForeignKey(Asset, on_delete=models.CASCADE, related_name="taggings")
|
|
tag = models.ForeignKey(AssetTag, on_delete=models.CASCADE, related_name="taggings")
|
|
|
|
class Meta:
|
|
unique_together = [("asset", "tag")]
|
|
|
|
|
|
class AssetUsage(TimeStampedModel):
|
|
asset = models.ForeignKey(Asset, on_delete=models.CASCADE, related_name="usages")
|
|
project = models.ForeignKey(
|
|
"projects.Project",
|
|
on_delete=models.CASCADE,
|
|
null=True,
|
|
blank=True,
|
|
related_name="asset_usages",
|
|
)
|
|
usage_type = models.CharField(max_length=64)
|
|
context = models.JSONField(default=dict, blank=True)
|
|
|
|
|
|
class FreeAssetGroup(TeamOwnedModel):
|
|
"""自由创作·人物素材组(一角色一组)。
|
|
|
|
本质是「火山 Assets API 引用登记表」:组内素材登记到火山素材库拿 remote_asset_id,
|
|
生成时以 asset://{remote_asset_id} 引用(免重复上传 + 火山侧预审)。
|
|
与 AssetReviewGroup(一团队一审核组)职责不同,与资产库 Asset(TOS 文件资产)也不同,独立建表。
|
|
"""
|
|
|
|
name = models.CharField(max_length=128)
|
|
description = models.TextField(blank=True)
|
|
remote_group_id = models.CharField(max_length=128)
|
|
thumbnail_url = models.URLField(blank=True)
|
|
is_deleted = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(fields=["team", "-created_at"]),
|
|
]
|
|
ordering = ["-created_at"]
|
|
|
|
def __str__(self) -> str:
|
|
return f"free-group:{self.name}"
|
|
|
|
|
|
class FreeAsset(TimeStampedModel):
|
|
"""自由创作·素材组内单素材(火山 Assets API 登记项)。"""
|
|
|
|
class Type(models.TextChoices):
|
|
IMAGE = "Image", "Image"
|
|
VIDEO = "Video", "Video"
|
|
AUDIO = "Audio", "Audio"
|
|
|
|
class Status(models.TextChoices):
|
|
PROCESSING = "processing", "Processing" # 火山侧处理/审核中
|
|
ACTIVE = "active", "Active" # 可用,可 asset:// 引用
|
|
FAILED = "failed", "Failed" # 审核/处理失败
|
|
|
|
group = models.ForeignKey(FreeAssetGroup, on_delete=models.CASCADE, related_name="assets")
|
|
remote_asset_id = models.CharField(max_length=128)
|
|
name = models.CharField(max_length=128, blank=True)
|
|
url = models.URLField(max_length=1024, blank=True) # TOS 公读直链(火山从这里拉源文件)
|
|
asset_type = models.CharField(max_length=16, choices=Type.choices, default=Type.IMAGE)
|
|
thumbnail_url = models.URLField(max_length=1024, blank=True)
|
|
duration = models.FloatField(null=True, blank=True) # 视频/音频时长(秒)
|
|
status = models.CharField(max_length=16, choices=Status.choices, default=Status.PROCESSING)
|
|
error_message = models.TextField(blank=True)
|
|
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(fields=["group", "-created_at"]),
|
|
models.Index(fields=["remote_asset_id"]),
|
|
]
|
|
ordering = ["-created_at"]
|
|
|
|
def __str__(self) -> str:
|
|
return f"free-asset:{self.name or self.remote_asset_id}"
|
|
|