Files
yingqing/core/backend/apps/assets/models.py
T
seaislee1209andClaude Opus 4.8 b30eaed838 feat(core): 火山人像素材库审核(绿/红盾)接入 + 端到端验证
- assets_client.py:照搬 AirDrama,volcengine SDK + AK/SK 签名(凭证走 settings.ASSETS_API)。
- review.py:真人资产送审/轮询编排。一团队一组(AssetReviewGroup,OneToOne),全 best-effort 不破坏主流程。
- 模型:AssetReviewGroup + Asset.review_status/review_remote_id/review_error(迁移 assets/0003)。
- 集成:真人基础资产生成后 transaction.on_commit 静默送审(services.generate_base_asset kind==person)。
- 端点 poll-reviews + 序列化暴露 review_status;前端基础资产趴每8s轮询、人物卡渲染 审核✓(绿)/审核✗·重生(红)/审核中,审核终态后停轮询。
- e2e 已验:送审→建组→processing→active(真拿到火山绿盾)。

对抗式交叉验证(2审查员,0 critical)修复:
- get_or_create_team_group 并发竞态:DB 唯一约束去重 + select_for_update 串行化远程建组,杜绝重复建组/丢送审。
- create_asset 返回空 Id 不再标 processing(否则卡死黄);processing 超 15 分钟兜底判 failed,未知 Status 记日志,防永久 processing + 无限轮询。

凭证暂借 AirDrama,.env 改 ASSETS_API_* 两行即可换;ASSETS_API_ENABLED=false 可一键关停。回归 18/18 过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:51:23 +08:00

116 lines
4.1 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"
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)
# 火山人像素材库审核(仅真人 person 资产):"" = 未送审 / processing 审核中 / active 绿盾通过 / failed 红标(需改提示词重生)
review_status = models.CharField(max_length=16, blank=True)
review_remote_id = models.CharField(max_length=128, blank=True)
review_error = models.TextField(blank=True)
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)